Optimize rendering by pre-loading resources #231

Merged
savanni merged 2 commits from otg/resource-loader into main 2024-04-09 13:56:03 +00:00
5 changed files with 171 additions and 49 deletions

View File

@ -14,7 +14,7 @@ GNU General Public License for more details.
You should have received a copy of the GNU General Public License along with On the Grid. If not, see <https://www.gnu.org/licenses/>. You should have received a copy of the GNU General Public License along with On the Grid. If not, see <https://www.gnu.org/licenses/>.
*/ */
use crate::CoreApi; use crate::{CoreApi, ResourceManager};
use adw::prelude::*; use adw::prelude::*;
use otg_core::{ use otg_core::{
@ -22,7 +22,7 @@ use otg_core::{
CoreRequest, CoreResponse, CoreRequest, CoreResponse,
}; };
use sgf::GameRecord; use sgf::GameRecord;
use std::sync::{Arc, RwLock}; use std::{rc::Rc, sync::{Arc, RwLock}};
use crate::views::{GameReview, HomeView, SettingsView}; use crate::views::{GameReview, HomeView, SettingsView};
@ -58,10 +58,12 @@ pub struct AppWindow {
// Not liking this, but I have to keep track of the settings view model separately from // Not liking this, but I have to keep track of the settings view model separately from
// anything else. I'll have to look into this later. // anything else. I'll have to look into this later.
settings_view_model: Arc<RwLock<Option<SettingsView>>>, settings_view_model: Arc<RwLock<Option<SettingsView>>>,
resources: ResourceManager,
} }
impl AppWindow { impl AppWindow {
pub fn new(app: &adw::Application, core: CoreApi) -> Self { pub fn new(app: &adw::Application, core: CoreApi, resources: ResourceManager) -> Self {
let window = Self::setup_window(app); let window = Self::setup_window(app);
let overlay = Self::setup_overlay(); let overlay = Self::setup_overlay();
let stack = adw::NavigationView::new(); let stack = adw::NavigationView::new();
@ -77,6 +79,7 @@ impl AppWindow {
overlay, overlay,
core, core,
settings_view_model: Default::default(), settings_view_model: Default::default(),
resources,
}; };
let home = s.setup_home(); let home = s.setup_home();
@ -88,7 +91,7 @@ impl AppWindow {
pub fn open_game_review(&self, game_record: GameRecord) { pub fn open_game_review(&self, game_record: GameRecord) {
let header = adw::HeaderBar::new(); let header = adw::HeaderBar::new();
let game_review = GameReview::new(self.core.clone(), game_record); let game_review = GameReview::new(self.core.clone(), game_record, self.resources.clone());
let layout = gtk::Box::builder() let layout = gtk::Box::builder()
.orientation(gtk::Orientation::Vertical) .orientation(gtk::Orientation::Vertical)

View File

@ -35,7 +35,7 @@ You should have received a copy of the GNU General Public License along with On
// Now, we know what kind of object we have for the current board representation. Let's make use of // Now, we know what kind of object we have for the current board representation. Let's make use of
// that. // that.
use crate::perftrace; use crate::{perftrace, Resource, ResourceManager};
use gio::resources_lookup_data; use gio::resources_lookup_data;
use glib::Object; use glib::Object;
@ -56,6 +56,7 @@ const MARGIN: i32 = 20;
#[derive(Default)] #[derive(Default)]
pub struct GobanPrivate { pub struct GobanPrivate {
board_state: Rc<RefCell<otg_core::Goban>>, board_state: Rc<RefCell<otg_core::Goban>>,
resource_manager: Rc<RefCell<Option<ResourceManager>>>,
} }
impl GobanPrivate {} impl GobanPrivate {}
@ -88,10 +89,11 @@ glib::wrapper! {
} }
impl Goban { impl Goban {
pub fn new(board_state: otg_core::Goban) -> Self { pub fn new(board_state: otg_core::Goban, resources: ResourceManager) -> Self {
let s: Self = Object::builder().build(); let s: Self = Object::builder().build();
*s.imp().board_state.borrow_mut() = board_state; *s.imp().board_state.borrow_mut() = board_state;
*s.imp().resource_manager.borrow_mut() = Some(resources);
s.set_width_request(WIDTH); s.set_width_request(WIDTH);
s.set_height_request(HEIGHT); s.set_height_request(HEIGHT);
@ -107,19 +109,42 @@ impl Goban {
fn redraw(&self, ctx: &cairo::Context, width: i32, height: i32) { fn redraw(&self, ctx: &cairo::Context, width: i32, height: i32) {
println!("{} x {}", width, height); println!("{} x {}", width, height);
/*
let background = load_pixbuf( let background = load_pixbuf(
"/com/luminescent-dreams/otg-gtk/wood_texture.jpg", "/com/luminescent-dreams/otg-gtk/wood_texture.jpg",
false, false,
WIDTH + 40, WIDTH + 40,
HEIGHT + 40, HEIGHT + 40,
); );
*/
let background = self
.imp()
.resource_manager
.borrow()
.as_ref()
.and_then(|r| r.resource("/com/luminescent-dreams/otg-gtk/wood_texture.jpg"));
let black_texture = self
.imp()
.resource_manager
.borrow()
.as_ref()
.and_then(|r| r.resource("/com/luminescent-dreams/otg-gtk/black_stone.png"));
let white_texture = self
.imp()
.resource_manager
.borrow()
.as_ref()
.and_then(|r| r.resource("/com/luminescent-dreams/otg-gtk/white_stone.png"));
match background { match background {
Ok(Some(ref background)) => { Some(Resource::Image(ref background)) => {
ctx.set_source_pixbuf(background, 0., 0.); ctx.set_source_pixbuf(background, 0., 0.);
ctx.paint().expect("paint should never fail"); ctx.paint().expect("paint should never fail");
} }
Ok(None) | Err(_) => ctx.set_source_rgb(0.7, 0.7, 0.7), None => ctx.set_source_rgb(0.7, 0.7, 0.7),
} }
let board = self.imp().board_state.borrow(); let board = self.imp().board_state.borrow();
@ -129,7 +154,14 @@ impl Goban {
let hspace_between = ((width - 40) as f64) / ((board.size.width - 1) as f64); let hspace_between = ((width - 40) as f64) / ((board.size.width - 1) as f64);
let vspace_between = ((height - 40) as f64) / ((board.size.height - 1) as f64); let vspace_between = ((height - 40) as f64) / ((board.size.height - 1) as f64);
let pen = Pen::new(MARGIN as f64, MARGIN as f64, hspace_between, vspace_between); let pen = Pen::new(
MARGIN as f64,
MARGIN as f64,
hspace_between,
vspace_between,
black_texture,
white_texture,
);
(0..board.size.width).for_each(|col| { (0..board.size.width).for_each(|col| {
ctx.move_to( ctx.move_to(
@ -179,35 +211,27 @@ struct Pen {
y_offset: f64, y_offset: f64,
hspace_between: f64, hspace_between: f64,
vspace_between: f64, vspace_between: f64,
black_stone: Pixbuf, black_stone: Option<Pixbuf>,
white_stone: Pixbuf, white_stone: Option<Pixbuf>,
} }
impl Pen { impl Pen {
fn new(x_offset: f64, y_offset: f64, hspace_between: f64, vspace_between: f64) -> Self { fn new(
let radius = (hspace_between / 2. - 2.) as i32; x_offset: f64,
let black_stone = load_pixbuf( y_offset: f64,
"/com/luminescent-dreams/otg-gtk/black_stone.png", hspace_between: f64,
true, vspace_between: f64,
512, black_stone: Option<Resource>,
512, white_stone: Option<Resource>,
) ) -> Self {
.unwrap() let black_stone = match black_stone {
.unwrap(); Some(Resource::Image(img)) => Some(img),
let black_stone = black_stone _ => None,
.scale_simple(radius * 2, radius * 2, InterpType::Nearest) };
.unwrap(); let white_stone = match white_stone {
let white_stone = load_pixbuf( Some(Resource::Image(img)) => Some(img),
"/com/luminescent-dreams/otg-gtk/white_stone.png", _ => None,
true, };
512,
512,
)
.unwrap()
.unwrap();
let white_stone = white_stone
.scale_simple(radius * 2, radius * 2, InterpType::Nearest)
.unwrap();
Pen { Pen {
x_offset, x_offset,
y_offset, y_offset,
@ -232,8 +256,14 @@ impl Pen {
fn stone(&self, ctx: &cairo::Context, row: u8, col: u8, color: Color, _liberties: Option<u8>) { fn stone(&self, ctx: &cairo::Context, row: u8, col: u8, color: Color, _liberties: Option<u8>) {
let (x_loc, y_loc) = self.stone_location(row, col); let (x_loc, y_loc) = self.stone_location(row, col);
match color { match color {
Color::White => ctx.set_source_pixbuf(&self.white_stone, x_loc, y_loc), Color::White => match self.white_stone {
Color::Black => ctx.set_source_pixbuf(&self.black_stone, x_loc, y_loc), Some(ref white_stone) => ctx.set_source_pixbuf(&white_stone, x_loc, y_loc),
None => ctx.set_source_rgb(0.9, 0.9, 0.9),
},
Color::Black => match self.black_stone {
Some(ref black_stone) => ctx.set_source_pixbuf(&black_stone, x_loc, y_loc),
None => ctx.set_source_rgb(0.0, 0.0, 0.0),
},
} }
ctx.paint().expect("paint should never fail"); ctx.paint().expect("paint should never fail");
/* /*

View File

@ -21,10 +21,12 @@ pub use app_window::AppWindow;
mod views; mod views;
use async_std::task::{yield_now}; use async_std::task::yield_now;
use otg_core::{Core, Observable, CoreRequest, CoreResponse}; use gio::resources_lookup_data;
use std::{rc::Rc}; use gtk::gdk_pixbuf::{Colorspace, InterpType, Pixbuf};
use image::{io::Reader as ImageReader, ImageError};
use otg_core::{Core, CoreRequest, CoreResponse, Observable};
use std::{cell::RefCell, collections::HashMap, io::Cursor, rc::Rc};
#[derive(Clone)] #[derive(Clone)]
pub struct CoreApi { pub struct CoreApi {
@ -37,6 +39,93 @@ impl CoreApi {
} }
} }
#[derive(Clone)]
pub enum Resource {
Image(Pixbuf),
}
#[derive(Clone)]
pub struct ResourceManager {
resources: Rc<RefCell<HashMap<String, Resource>>>,
}
impl ResourceManager {
pub fn new() -> Self {
let mut resources = HashMap::new();
for (path, xres, yres, transparency) in [
(
"/com/luminescent-dreams/otg-gtk/wood_texture.jpg",
840,
840,
false,
),
(
"/com/luminescent-dreams/otg-gtk/black_stone.png",
40,
40,
true,
),
(
"/com/luminescent-dreams/otg-gtk/white_stone.png",
40,
40,
true,
),
] {
match perftrace(&format!("loading {}", path), || {
Self::load_image(path, transparency, xres, yres)
}) {
Ok(Some(image)) => {
resources.insert(path.to_owned(), Resource::Image(image));
}
Ok(None) => println!("no image in resource bundle for {}", path),
Err(err) => println!("failed to load image {}: {}", path, err),
}
}
Self {
resources: Rc::new(RefCell::new(resources)),
}
}
pub fn resource(&self, path: &str) -> Option<Resource> {
self.resources.borrow().get(path).cloned()
}
fn load_image(
path: &str,
transparency: bool,
width: i32,
height: i32,
) -> Result<Option<Pixbuf>, ImageError> {
let image_bytes = resources_lookup_data(path, gio::ResourceLookupFlags::NONE).unwrap();
let image = ImageReader::new(Cursor::new(image_bytes))
.with_guessed_format()
.unwrap()
.decode();
image.map(|image| {
let stride = if transparency {
image.to_rgba8().sample_layout().height_stride
} else {
image.to_rgb8().sample_layout().height_stride
};
Pixbuf::from_bytes(
&glib::Bytes::from(image.as_bytes()),
Colorspace::Rgb,
transparency,
8,
image.width() as i32,
image.height() as i32,
stride as i32,
)
.scale_simple(width, height, InterpType::Nearest)
})
}
}
pub fn perftrace<F, A>(trace_name: &str, f: F) -> A pub fn perftrace<F, A>(trace_name: &str, f: F) -> A
where where
F: FnOnce() -> A, F: FnOnce() -> A,

View File

@ -4,8 +4,7 @@ use async_std::task::spawn;
use gio::ActionEntry; use gio::ActionEntry;
use otg_core::{Config, ConfigOption, Core, CoreNotification, LibraryPath, Observable}; use otg_core::{Config, ConfigOption, Core, CoreNotification, LibraryPath, Observable};
use otg_gtk::{ use otg_gtk::{
AppWindow, AppWindow, CoreApi, ResourceManager
CoreApi,
}; };
@ -123,8 +122,9 @@ fn main() {
app.connect_activate({ app.connect_activate({
move |app| { move |app| {
let resources = ResourceManager::new();
let core_api = CoreApi { core: core.clone() }; let core_api = CoreApi { core: core.clone() };
let app_window = AppWindow::new(app, core_api); let app_window = AppWindow::new(app, core_api, resources);
setup_app_configuration_action(app, app_window.clone()); setup_app_configuration_action(app, app_window.clone());

View File

@ -22,7 +22,9 @@ You should have received a copy of the GNU General Public License along with On
// I'll get all of the information about the game from the core, and then render everything in the // I'll get all of the information about the game from the core, and then render everything in the
// UI. So this will be a heavy lift on the UI side. // UI. So this will be a heavy lift on the UI side.
use crate::{components::{Goban, PlayerCard, ReviewTree}, CoreApi}; use crate::{
components::{Goban, PlayerCard, ReviewTree}, CoreApi, ResourceManager
};
use glib::Object; use glib::Object;
use gtk::{prelude::*, subclass::prelude::*}; use gtk::{prelude::*, subclass::prelude::*};
use otg_core::Color; use otg_core::Color;
@ -31,8 +33,6 @@ use sgf::GameRecord;
#[derive(Default)] #[derive(Default)]
pub struct GameReviewPrivate {} pub struct GameReviewPrivate {}
#[glib::object_subclass] #[glib::object_subclass]
impl ObjectSubclass for GameReviewPrivate { impl ObjectSubclass for GameReviewPrivate {
const NAME: &'static str = "GameReview"; const NAME: &'static str = "GameReview";
@ -49,7 +49,7 @@ glib::wrapper! {
} }
impl GameReview { impl GameReview {
pub fn new(_api: CoreApi, record: GameRecord) -> Self { pub fn new(_api: CoreApi, record: GameRecord, resources: ResourceManager) -> Self {
let s: Self = Object::builder().build(); let s: Self = Object::builder().build();
// It's actually really bad to be just throwing away errors. Panics make everyone unhappy. // It's actually really bad to be just throwing away errors. Panics make everyone unhappy.
@ -58,7 +58,7 @@ impl GameReview {
let board_repr = otg_core::Goban::default() let board_repr = otg_core::Goban::default()
.apply_moves(record.mainline()) .apply_moves(record.mainline())
.unwrap(); .unwrap();
let board = Goban::new(board_repr); let board = Goban::new(board_repr, resources);
/* /*
s.attach(&board, 0, 0, 2, 2); s.attach(&board, 0, 0, 2, 2);