♻️ Refactor GpuState and RenderState

* ♻️ Refactor GpuState

* ♻️ Refactor RenderState

* 🔧 Tweak some _build_env options
This commit is contained in:
Aitor Moreno 2026-05-08 11:10:14 +02:00 committed by GitHub
parent cccd7bc6de
commit 4e98dfb99f
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
14 changed files with 232 additions and 269 deletions

View File

@ -42,6 +42,8 @@ export EMCC_CFLAGS="--no-entry \
-sEXPORTED_RUNTIME_METHODS=GL,UTF8ToString,stringToUTF8,HEAPU8,HEAP32,HEAPU32,HEAPF32 \
-sENVIRONMENT=web \
-sMODULARIZE=1 \
-sDISABLE_EXCEPTION_CATCHING=1 \
-sFILESYSTEM=0 \
-sEXPORT_ES6=1";
export EM_CACHE="/tmp/emsdk_cache";

View File

@ -22,6 +22,7 @@ use crate::state::TextEditorState;
use macros::wasm_error;
use math::{Bounds, Matrix};
use mem::SerializableResult;
use render::{gpu_state::GpuState, RenderState};
use shapes::{StructureEntry, StructureEntryType, TransformEntry};
use skia_safe as skia;
use state::State;
@ -39,6 +40,28 @@ pub fn get_text_editor_state() -> &'static mut TextEditorState {
}
}
/// GPU State.
static mut GPU_STATE: *mut GpuState = std::ptr::null_mut();
#[inline(always)]
pub(crate) fn get_gpu_state() -> &'static mut GpuState {
unsafe {
debug_assert!(!GPU_STATE.is_null(), "GPU State is null");
&mut *GPU_STATE
}
}
/// Render State.
static mut RENDER_STATE: *mut RenderState = std::ptr::null_mut();
#[inline(always)]
pub(crate) fn get_render_state() -> &'static mut RenderState {
unsafe {
debug_assert!(!RENDER_STATE.is_null(), "Render State is null");
&mut *RENDER_STATE
}
}
// FIXME: These with_state* macros should be using our CriticalError instead of expect.
// But to do that, we need to not use them at domain-level (i.e. in business logic), just
// in the context of the wasm call.
@ -111,11 +134,30 @@ macro_rules! with_state_mut_current_shape {
};
}
/// Initializes GPU.
fn gpu_init() {
unsafe {
let gpu_state = GpuState::try_new().expect("Cannot initialize GPU State");
GPU_STATE = Box::into_raw(Box::new(gpu_state));
}
}
/// Initializes RenderState.
fn render_init(width: i32, height: i32) {
unsafe {
let render_state =
RenderState::try_new(width, height).expect("Cannot intialize RenderState");
RENDER_STATE = Box::into_raw(Box::new(render_state));
}
}
#[no_mangle]
#[wasm_error]
pub extern "C" fn init(width: i32, height: i32) -> Result<()> {
let state_box = Box::new(State::try_new(width, height)?);
gpu_init();
render_init(width, height);
unsafe {
let state_box = Box::new(State::new());
STATE = Some(state_box);
TEXT_EDITOR_STATE = Box::into_raw(Box::new(TextEditorState::new()));
}
@ -134,12 +176,10 @@ pub extern "C" fn set_browser(browser: u8) -> Result<()> {
#[no_mangle]
#[wasm_error]
pub extern "C" fn clean_up() -> Result<()> {
with_state_mut!(state, {
// Cancel the current animation frame if it exists so
// it won't try to render without context
let render_state = state.render_state_mut();
render_state.cancel_animation_frame();
});
// Cancel the current animation frame if it exists so
// it won't try to render without context
let render_state = get_render_state();
render_state.cancel_animation_frame();
unsafe { STATE = None }
mem::free_bytes()?;
Ok(())
@ -148,11 +188,9 @@ pub extern "C" fn clean_up() -> Result<()> {
#[no_mangle]
#[wasm_error]
pub extern "C" fn set_render_options(debug: u32, dpr: f32) -> Result<()> {
with_state_mut!(state, {
let render_state = state.render_state_mut();
render_state.set_debug_flags(debug);
render_state.set_dpr(dpr)?;
});
let render_state = get_render_state();
render_state.set_debug_flags(debug);
render_state.set_dpr(dpr)?;
Ok(())
}
@ -161,61 +199,48 @@ pub extern "C" fn set_render_options(debug: u32, dpr: f32) -> Result<()> {
pub extern "C" fn set_viewport_interest_area_threshold(
viewport_interest_area_threshold: i32,
) -> Result<()> {
with_state_mut!(state, {
let render_state = state.render_state_mut();
render_state.set_viewport_interest_area_threshold(viewport_interest_area_threshold);
});
let render_state = get_render_state();
render_state.set_viewport_interest_area_threshold(viewport_interest_area_threshold);
Ok(())
}
#[no_mangle]
#[wasm_error]
pub extern "C" fn set_max_blocking_time_ms(max_blocking_time_ms: i32) -> Result<()> {
with_state_mut!(state, {
let render_state = state.render_state_mut();
render_state.set_max_blocking_time_ms(max_blocking_time_ms);
});
let render_state = get_render_state();
render_state.set_max_blocking_time_ms(max_blocking_time_ms);
Ok(())
}
#[no_mangle]
#[wasm_error]
pub extern "C" fn set_node_batch_threshold(node_batch_threshold: i32) -> Result<()> {
with_state_mut!(state, {
let render_state = state.render_state_mut();
render_state.set_node_batch_threshold(node_batch_threshold);
});
let render_state = get_render_state();
render_state.set_node_batch_threshold(node_batch_threshold);
Ok(())
}
#[no_mangle]
#[wasm_error]
pub extern "C" fn set_blur_downscale_threshold(blur_downscale_threshold: f32) -> Result<()> {
with_state_mut!(state, {
let render_state = state.render_state_mut();
render_state.set_blur_downscale_threshold(blur_downscale_threshold);
});
let render_state = get_render_state();
render_state.set_blur_downscale_threshold(blur_downscale_threshold);
Ok(())
}
#[no_mangle]
#[wasm_error]
pub extern "C" fn set_antialias_threshold(threshold: f32) -> Result<()> {
with_state_mut!(state, {
state.render_state_mut().set_antialias_threshold(threshold);
});
get_render_state().set_antialias_threshold(threshold);
Ok(())
}
#[no_mangle]
#[wasm_error]
pub extern "C" fn set_max_atlas_texture_size(max_px: i32) -> Result<()> {
with_state_mut!(state, {
state
.render_state_mut()
.surfaces
.set_max_atlas_texture_size(max_px);
});
get_render_state()
.surfaces
.set_max_atlas_texture_size(max_px);
Ok(())
}
@ -241,7 +266,7 @@ pub extern "C" fn render(timestamp: i32) -> Result<()> {
// interactive_transform; we do it once here, with the current
// modifier set, so the cost is paid once per rAF rather than
// once per pointer move.
if state.render_state.options.is_interactive_transform() {
if get_render_state().options.is_interactive_transform() {
let ids = state.shapes.modifier_ids();
if !ids.is_empty() {
state.rebuild_modifier_tiles(ids)?;
@ -311,9 +336,7 @@ pub extern "C" fn render_from_cache(_: i32) -> Result<()> {
#[no_mangle]
#[wasm_error]
pub extern "C" fn set_preview_mode(enabled: bool) -> Result<()> {
with_state_mut!(state, {
state.render_state.set_preview_mode(enabled);
});
get_render_state().set_preview_mode(enabled);
Ok(())
}
@ -356,9 +379,7 @@ pub extern "C" fn end_loading() -> Result<()> {
#[no_mangle]
#[wasm_error]
pub extern "C" fn render_loading_overlay() -> Result<()> {
with_state_mut!(state, {
state.render_state.render_loading_overlay();
});
get_render_state().render_loading_overlay();
Ok(())
}
@ -375,30 +396,24 @@ pub extern "C" fn process_animation_frame(timestamp: i32) -> Result<()> {
#[no_mangle]
#[wasm_error]
pub extern "C" fn reset_canvas() -> Result<()> {
with_state_mut!(state, {
state.render_state_mut().reset_canvas();
});
get_render_state().reset_canvas();
Ok(())
}
#[no_mangle]
#[wasm_error]
pub extern "C" fn resize_viewbox(width: i32, height: i32) -> Result<()> {
with_state_mut!(state, {
state.resize(width, height)?;
});
get_render_state().resize(width, height)?;
Ok(())
}
#[no_mangle]
#[wasm_error]
pub extern "C" fn set_view(zoom: f32, x: f32, y: f32) -> Result<()> {
with_state_mut!(state, {
performance::begin_measure!("set_view");
let render_state = state.render_state_mut();
render_state.set_view(zoom, x, y);
performance::end_measure!("set_view");
});
performance::begin_measure!("set_view");
let render_state = get_render_state();
render_state.set_view(zoom, x, y);
performance::end_measure!("set_view");
Ok(())
}
@ -408,15 +423,13 @@ static mut VIEW_INTERACTION_START: i32 = 0;
#[no_mangle]
#[wasm_error]
pub extern "C" fn set_view_start() -> Result<()> {
with_state_mut!(state, {
#[cfg(feature = "profile-macros")]
unsafe {
VIEW_INTERACTION_START = performance::get_time();
}
performance::begin_measure!("set_view_start");
state.render_state.options.set_fast_mode(true);
performance::end_measure!("set_view_start");
});
#[cfg(feature = "profile-macros")]
unsafe {
VIEW_INTERACTION_START = performance::get_time();
}
performance::begin_measure!("set_view_start");
get_render_state().options.set_fast_mode(true);
performance::end_measure!("set_view_start");
Ok(())
}
@ -430,32 +443,32 @@ pub extern "C" fn set_view_start() -> Result<()> {
pub extern "C" fn set_view_end() -> Result<()> {
with_state_mut!(state, {
performance::begin_measure!("set_view_end");
state.render_state.options.set_fast_mode(false);
state.render_state.cancel_animation_frame();
let render_state = get_render_state();
render_state.options.set_fast_mode(false);
render_state.cancel_animation_frame();
let scale = state.render_state.get_scale();
state
.render_state
let scale = render_state.get_scale();
render_state
.tile_viewbox
.update(state.render_state.viewbox, scale);
.update(render_state.viewbox, scale);
if state.render_state.options.is_profile_rebuild_tiles() {
if render_state.options.is_profile_rebuild_tiles() {
state.rebuild_tiles();
} else if state.render_state.zoom_changed() {
} else if render_state.zoom_changed() {
// Zoom changed: tile sizes differ so all cached tile
// textures are invalid (wrong scale). Rebuild the tile
// index and clear the tile texture cache, but *preserve*
// the cache canvas so render_from_cache can show a scaled
// preview of the old content while new tiles render.
state.render_state.rebuild_tile_index(&state.shapes);
state.render_state.surfaces.invalidate_tile_cache();
render_state.rebuild_tile_index(&state.shapes);
render_state.surfaces.invalidate_tile_cache();
} else {
// Pure pan at the same zoom level: tile contents have not
// changed — only the viewport position moved. Update the
// tile index (which tiles are in the interest area) but
// keep cached tile textures so the render can blit them
// instead of re-drawing every visible tile from scratch.
state.render_state.rebuild_tile_index(&state.shapes);
render_state.rebuild_tile_index(&state.shapes);
}
performance::end_measure!("set_view_end");
});
@ -470,12 +483,11 @@ pub extern "C" fn set_view_end() -> Result<()> {
#[no_mangle]
#[wasm_error]
pub extern "C" fn set_modifiers_start() -> Result<()> {
with_state_mut!(state, {
performance::begin_measure!("set_modifiers_start");
state.render_state.options.set_fast_mode(true);
state.render_state.options.set_interactive_transform(true);
performance::end_measure!("set_modifiers_start");
});
performance::begin_measure!("set_modifiers_start");
let render_state = get_render_state();
render_state.options.set_fast_mode(true);
render_state.options.set_interactive_transform(true);
performance::end_measure!("set_modifiers_start");
Ok(())
}
@ -486,13 +498,12 @@ pub extern "C" fn set_modifiers_start() -> Result<()> {
#[no_mangle]
#[wasm_error]
pub extern "C" fn set_modifiers_end() -> Result<()> {
with_state_mut!(state, {
performance::begin_measure!("set_modifiers_end");
state.render_state.options.set_fast_mode(false);
state.render_state.options.set_interactive_transform(false);
state.render_state.cancel_animation_frame();
performance::end_measure!("set_modifiers_end");
});
performance::begin_measure!("set_modifiers_end");
let render_state = get_render_state();
render_state.options.set_fast_mode(false);
render_state.options.set_interactive_transform(false);
render_state.cancel_animation_frame();
performance::end_measure!("set_modifiers_end");
Ok(())
}
@ -808,11 +819,9 @@ pub extern "C" fn is_image_cached(
d: u32,
is_thumbnail: bool,
) -> Result<bool> {
with_state_mut!(state, {
let id = uuid_from_u32_quartet(a, b, c, d);
let result = state.render_state().has_image(&id, is_thumbnail);
Ok(result)
})
let id = uuid_from_u32_quartet(a, b, c, d);
let result = get_render_state().has_image(&id, is_thumbnail);
Ok(result)
}
#[no_mangle]
@ -961,15 +970,14 @@ pub extern "C" fn set_structure_modifiers() -> Result<()> {
#[wasm_error]
pub extern "C" fn clean_modifiers() -> Result<()> {
with_state_mut!(state, {
let render_state = get_render_state();
let prev_modifier_ids = state.shapes.clean_all();
// Skip the tile-cache cleanup during interactive transform: the
// per-rAF `rebuild_modifier_tiles` in `render()` already evicts
// the same tiles for the active modifier set, so the eviction
// here is redundant and doubles the per-emission cost.
if !prev_modifier_ids.is_empty() && !state.render_state.options.is_interactive_transform() {
state
.render_state
.update_tiles_shapes(&prev_modifier_ids, &mut state.shapes)?;
if !prev_modifier_ids.is_empty() && !render_state.options.is_interactive_transform() {
render_state.update_tiles_shapes(&prev_modifier_ids, &mut state.shapes)?;
}
});
Ok(())
@ -995,7 +1003,7 @@ pub extern "C" fn set_modifiers() -> Result<()> {
with_state_mut!(state, {
state.set_modifiers(modifiers);
// TO CHECK
if !state.render_state.options.is_interactive_transform() {
if !get_render_state().options.is_interactive_transform() {
state.rebuild_modifier_tiles(ids)?;
}
});
@ -1061,9 +1069,7 @@ pub extern "C" fn render_shape_pixels(
#[no_mangle]
pub extern "C" fn render_stats() {
with_state!(state, {
state.render_state.print_stats();
})
get_render_state().print_stats();
}
fn main() {

View File

@ -2,7 +2,7 @@ mod debug;
mod fills;
pub mod filters;
mod fonts;
mod gpu_state;
pub mod gpu_state;
pub mod grid_layout;
mod images;
mod options;
@ -17,13 +17,10 @@ use skia_safe::{self as skia, Matrix, RRect, Rect};
use std::borrow::Cow;
use std::collections::{HashMap, HashSet};
use gpu_state::GpuState;
use options::RenderOptions;
pub use surfaces::{SurfaceId, Surfaces};
use crate::error::{Error, Result};
use crate::performance;
use crate::shapes::{
all_with_ancestors, radius_to_sigma, Blur, BlurType, Corners, Fill, Shadow, Shape, SolidColor,
Stroke, StrokeKind, TextContent, Type,
@ -33,6 +30,7 @@ use crate::tiles::{self, PendingTiles, TileRect};
use crate::uuid::Uuid;
use crate::view::Viewbox;
use crate::wapi;
use crate::{get_gpu_state, performance};
pub use fonts::*;
pub use images::*;
@ -327,7 +325,6 @@ impl RenderStats {
}
pub(crate) struct RenderState {
gpu_state: GpuState,
pub options: RenderOptions,
stats: RenderStats,
pub surfaces: Surfaces,
@ -492,13 +489,11 @@ impl RenderState {
pub fn try_new(width: i32, height: i32) -> Result<RenderState> {
// This needs to be done once per WebGL context.
let mut gpu_state = GpuState::try_new()?;
let sampling_options =
skia::SamplingOptions::new(skia::FilterMode::Linear, skia::MipmapMode::Nearest);
let fonts = FontStore::try_new()?;
let surfaces = Surfaces::try_new(
&mut gpu_state,
(width, height),
sampling_options,
tiles::get_tile_dimensions(),
@ -511,15 +506,14 @@ impl RenderState {
let tiles = tiles::TileHashMap::new();
let options = RenderOptions::default();
Ok(RenderState {
gpu_state: gpu_state.clone(),
Ok(Self {
options,
stats: RenderStats::new(),
surfaces,
fonts,
viewbox,
cached_viewbox: Viewbox::new(0., 0.),
images: ImageStore::new(gpu_state.context.clone()),
images: ImageStore::new(),
background_color: skia::Color::TRANSPARENT,
render_request_id: None,
render_in_progress: false,
@ -801,8 +795,7 @@ impl RenderState {
pub fn resize(&mut self, width: i32, height: i32) -> Result<()> {
let dpr_width = (width as f32 * self.options.dpr).floor() as i32;
let dpr_height = (height as f32 * self.options.dpr).floor() as i32;
self.surfaces
.resize(&mut self.gpu_state, dpr_width, dpr_height)?;
self.surfaces.resize(dpr_width, dpr_height)?;
self.viewbox.set_wh(width as f32, height as f32);
self.tile_viewbox.update(self.viewbox, self.get_scale());
@ -810,8 +803,7 @@ impl RenderState {
}
pub fn flush_and_submit(&mut self) {
self.surfaces
.flush_and_submit(&mut self.gpu_state, SurfaceId::Target);
self.surfaces.flush_and_submit(SurfaceId::Target);
}
pub fn reset_canvas(&mut self) {
@ -890,7 +882,6 @@ impl RenderState {
.as_ref()
.ok_or(Error::CriticalError("Current tile not found".to_string()))?;
self.surfaces.cache_current_tile_texture(
&mut self.gpu_state,
&self.tile_viewbox,
&current_tile,
&tile_rect,
@ -2203,13 +2194,12 @@ impl RenderState {
// Clear export context so get_scale() returns to workspace zoom.
self.export_context = None;
self.surfaces
.flush_and_submit(&mut self.gpu_state, target_surface);
self.surfaces.flush_and_submit(target_surface);
let image = self.surfaces.snapshot(target_surface);
let data = image
.encode(
&mut self.gpu_state.context,
Some(&mut get_gpu_state().context),
skia::EncodedImageFormat::PNG,
100,
)
@ -3590,8 +3580,7 @@ impl RenderState {
}
pub fn remove_cached_tile(&mut self, tile: tiles::Tile) {
self.surfaces
.remove_cached_tile_surface(&mut self.gpu_state, tile);
self.surfaces.remove_cached_tile_surface(tile);
}
/// Rebuild the tile index (shape→tile mapping) for all top-level shapes.
@ -3790,6 +3779,6 @@ impl RenderState {
impl Drop for RenderState {
fn drop(&mut self) {
self.gpu_state.context.free_gpu_resources();
get_gpu_state().context.free_gpu_resources();
}
}

View File

@ -1,7 +1,11 @@
use super::{tiles, RenderState, SurfaceId};
use crate::with_state_mut;
use crate::STATE;
#[cfg(target_arch = "wasm32")]
use macros::wasm_error;
#[cfg(target_arch = "wasm32")]
use crate::get_render_state;
use skia_safe::{self as skia, Rect};
#[cfg(target_arch = "wasm32")]
@ -227,9 +231,7 @@ pub fn console_debug_surface_rect(render_state: &mut RenderState, id: SurfaceId,
#[wasm_error]
#[cfg(target_arch = "wasm32")]
pub extern "C" fn debug_cache_console() -> Result<()> {
with_state_mut!(state, {
console_debug_surface(state.render_state_mut(), SurfaceId::Cache);
});
console_debug_surface(get_render_state(), SurfaceId::Cache);
Ok(())
}
@ -237,9 +239,7 @@ pub extern "C" fn debug_cache_console() -> Result<()> {
#[wasm_error]
#[cfg(target_arch = "wasm32")]
pub extern "C" fn debug_cache_base64() -> Result<()> {
with_state_mut!(state, {
console_debug_surface_base64(state.render_state_mut(), SurfaceId::Cache);
});
console_debug_surface_base64(get_render_state(), SurfaceId::Cache);
Ok(())
}
@ -247,9 +247,7 @@ pub extern "C" fn debug_cache_base64() -> Result<()> {
#[wasm_error]
#[cfg(target_arch = "wasm32")]
pub extern "C" fn debug_atlas_console() -> Result<()> {
with_state_mut!(state, {
console_debug_surface(state.render_state_mut(), SurfaceId::Atlas);
});
console_debug_surface(get_render_state(), SurfaceId::Atlas);
Ok(())
}
@ -257,8 +255,6 @@ pub extern "C" fn debug_atlas_console() -> Result<()> {
#[wasm_error]
#[cfg(target_arch = "wasm32")]
pub extern "C" fn debug_atlas_base64() -> Result<()> {
with_state_mut!(state, {
console_debug_surface_base64(state.render_state_mut(), SurfaceId::Atlas);
});
console_debug_surface_base64(get_render_state(), SurfaceId::Atlas);
Ok(())
}

View File

@ -41,7 +41,7 @@ impl GpuState {
}
};
Ok(GpuState {
Ok(Self {
context,
framebuffer_info,
})

View File

@ -3,6 +3,7 @@ use crate::shapes::ImageFill;
use crate::uuid::Uuid;
use crate::error::Result;
use crate::get_gpu_state;
use skia_safe::gpu::{surfaces, Budgeted, DirectContext};
use skia_safe::{self as skia, Codec, ISize};
use std::collections::HashMap;
@ -143,10 +144,12 @@ fn decode_image(context: &mut Box<DirectContext>, raw_data: &[u8]) -> Option<Ima
}
impl ImageStore {
pub fn new(context: DirectContext) -> Self {
pub fn new() -> Self {
let gpu_state = get_gpu_state();
let context = &gpu_state.context;
Self {
images: HashMap::with_capacity(2048),
context: Box::new(context),
context: Box::new(context.clone()),
}
}

View File

@ -1,7 +1,7 @@
use crate::error::{Error, Result};
use crate::performance;
use crate::shapes::Shape;
use crate::view::Viewbox;
use crate::{get_gpu_state, performance};
use skia_safe::{self as skia, IRect, Paint, RRect};
@ -101,11 +101,12 @@ pub struct Surfaces {
#[allow(dead_code)]
impl Surfaces {
pub fn try_new(
gpu_state: &mut GpuState,
(width, height): (i32, i32),
sampling_options: skia::SamplingOptions,
tile_dims: skia::ISize,
) -> Result<Self> {
let gpu_state = get_gpu_state();
let extra_tile_dims = skia::ISize::new(
tile_dims.width * TILE_SIZE_MULTIPLIER,
tile_dims.height * TILE_SIZE_MULTIPLIER,
@ -140,7 +141,7 @@ impl Surfaces {
atlas.canvas().clear(skia::Color::TRANSPARENT);
let tiles = TileTextureCache::new();
Ok(Surfaces {
Ok(Self {
target,
filter,
cache,
@ -445,12 +446,9 @@ impl Surfaces {
self.margins
}
pub fn resize(
&mut self,
gpu_state: &mut GpuState,
new_width: i32,
new_height: i32,
) -> Result<()> {
pub fn resize(&mut self, new_width: i32, new_height: i32) -> Result<()> {
let gpu_state = get_gpu_state();
self.reset_from_target(gpu_state.create_target_surface(new_width, new_height)?)?;
Ok(())
}
@ -547,7 +545,8 @@ impl Surfaces {
self.dirty_surfaces = 0;
}
pub fn flush_and_submit(&mut self, gpu_state: &mut GpuState, id: SurfaceId) {
pub fn flush_and_submit(&mut self, id: SurfaceId) {
let gpu_state = get_gpu_state();
let surface = self.get_mut(id);
gpu_state.context.flush_and_submit_surface(surface, None);
}
@ -946,13 +945,13 @@ impl Surfaces {
pub fn cache_current_tile_texture(
&mut self,
gpu_state: &mut GpuState,
tile_viewbox: &TileViewbox,
tile: &Tile,
tile_rect: &skia::Rect,
skip_cache_surface: bool,
tile_doc_rect: skia::Rect,
) {
let gpu_state = get_gpu_state();
let rect = IRect::from_xywh(
self.margins.width,
self.margins.height,
@ -985,7 +984,8 @@ impl Surfaces {
self.tiles.has(tile)
}
pub fn remove_cached_tile_surface(&mut self, gpu_state: &mut GpuState, tile: Tile) {
pub fn remove_cached_tile_surface(&mut self, tile: Tile) {
let gpu_state = get_gpu_state();
// Mark tile as invalid
// Old content stays visible until new tile overwrites it atomically,
// preventing flickering during tile re-renders.

View File

@ -1,11 +1,10 @@
use crate::get_render_state;
use crate::shapes::text::TextContent;
use skia_safe::{
self as skia, textlayout::Paragraph as SkiaParagraph, FontMetrics, Point, Rect, TextBlob,
};
use std::ops::Deref;
use crate::{with_state_mut, STATE};
pub struct TextPaths(TextContent);
// Note: This class is not being currently used.
@ -173,20 +172,18 @@ impl TextPaths {
blob_offset_x: f32,
blob_offset_y: f32,
) -> Option<(skia::Path, skia::Rect)> {
with_state_mut!(state, {
let utf16_text = span_text.encode_utf16().collect::<Vec<u16>>();
let text = unsafe { skia_safe::as_utf16_unchecked(&utf16_text) };
let emoji_font = state.render_state.fonts().get_emoji_font(font.size());
let use_font = emoji_font.as_ref().unwrap_or(font);
let utf16_text = span_text.encode_utf16().collect::<Vec<u16>>();
let text = unsafe { skia_safe::as_utf16_unchecked(&utf16_text) };
let emoji_font = get_render_state().fonts().get_emoji_font(font.size());
let use_font = emoji_font.as_ref().unwrap_or(font);
if let Some(mut text_blob) = TextBlob::from_text(text, use_font) {
let path = SkiaParagraph::get_path(&mut text_blob);
let d = Point::new(blob_offset_x, blob_offset_y);
let offset_path = path.with_offset(d);
let bounds = text_blob.bounds();
return Some((offset_path, *bounds));
}
});
if let Some(mut text_blob) = TextBlob::from_text(text, use_font) {
let path = SkiaParagraph::get_path(&mut text_blob);
let d = Point::new(blob_offset_x, blob_offset_y);
let offset_path = path.with_offset(d);
let bounds = text_blob.bounds();
return Some((offset_path, *bounds));
}
None
}
}

View File

@ -7,10 +7,9 @@ pub use shapes_pool::{ShapesPool, ShapesPoolMutRef, ShapesPoolRef};
pub use text_editor::*;
use crate::error::{Error, Result};
use crate::render::RenderState;
use crate::shapes::{modifiers::grid_layout::grid_cell_data, Shape};
use crate::tiles;
use crate::shapes::{grid_layout::grid_cell_data, Shape};
use crate::uuid::Uuid;
use crate::{get_render_state, tiles};
/// This struct holds the state of the Rust application between JS calls.
///
@ -18,7 +17,6 @@ use crate::uuid::Uuid;
/// Note that rust-skia data structures are not thread safe, so a state
/// must not be shared between different Web Workers.
pub(crate) struct State {
pub render_state: RenderState,
pub current_id: Option<Uuid>,
pub current_browser: u8,
pub shapes: ShapesPool,
@ -28,16 +26,14 @@ pub(crate) struct State {
}
impl State {
pub fn try_new(width: i32, height: i32) -> Result<Self> {
Ok(State {
render_state: RenderState::try_new(width, height)?,
// text_editor_state: TextEditorState::new(),
pub fn new() -> Self {
Self {
current_id: None,
current_browser: 0,
shapes: ShapesPool::new(),
saved_shapes: None,
loading: false,
})
}
}
// Creates a new temporary shapes pool.
@ -64,30 +60,16 @@ impl State {
Ok(self)
}
pub fn resize(&mut self, width: i32, height: i32) -> Result<()> {
self.render_state.resize(width, height)
}
pub fn render_state_mut(&mut self) -> &mut RenderState {
&mut self.render_state
}
pub fn render_state(&self) -> &RenderState {
&self.render_state
}
pub fn render_from_cache(&mut self) {
self.render_state.render_from_cache(&self.shapes);
get_render_state().render_from_cache(&self.shapes);
}
pub fn render_sync(&mut self, timestamp: i32) -> Result<()> {
self.render_state
.start_render_loop(None, &self.shapes, timestamp, true)
get_render_state().start_render_loop(None, &self.shapes, timestamp, true)
}
pub fn render_sync_shape(&mut self, id: &Uuid, timestamp: i32) -> Result<()> {
self.render_state
.start_render_loop(Some(id), &self.shapes, timestamp, true)
get_render_state().start_render_loop(Some(id), &self.shapes, timestamp, true)
}
pub fn render_shape_pixels(
@ -96,36 +78,34 @@ impl State {
scale: f32,
timestamp: i32,
) -> Result<(Vec<u8>, i32, i32)> {
self.render_state
.render_shape_pixels(id, &self.shapes, scale, timestamp)
get_render_state().render_shape_pixels(id, &self.shapes, scale, timestamp)
}
pub fn start_render_loop(&mut self, timestamp: i32) -> Result<()> {
let render_state = get_render_state();
// If zoom changed (e.g. interrupted zoom render followed by pan), the
// tile index may be stale for the new viewport position. Rebuild the
// index so shapes are mapped to the correct tiles. We use
// rebuild_tile_index (NOT rebuild_tiles_shallow) to preserve the tile
// texture cache — otherwise cached tiles with shadows/blur would be
// cleared and re-rendered in fast mode without effects.
if self.render_state.zoom_changed() {
self.render_state.rebuild_tile_index(&self.shapes);
if render_state.zoom_changed() {
render_state.rebuild_tile_index(&self.shapes);
}
self.render_state
.start_render_loop(None, &self.shapes, timestamp, false)
render_state.start_render_loop(None, &self.shapes, timestamp, false)
}
pub fn process_animation_frame(&mut self, timestamp: i32) -> Result<()> {
self.render_state
.process_animation_frame(None, &self.shapes, timestamp)
get_render_state().process_animation_frame(None, &self.shapes, timestamp)
}
pub fn clear_focus_mode(&mut self) {
self.render_state.clear_focus_mode();
get_render_state().clear_focus_mode();
}
pub fn set_focus_mode(&mut self, shapes: Vec<Uuid>) {
self.render_state.set_focus_mode(shapes);
get_render_state().set_focus_mode(shapes);
}
pub fn init_shapes_pool(&mut self, capacity: usize) {
@ -140,6 +120,8 @@ impl State {
}
pub fn delete_shape_children(&mut self, parent_id: Uuid, id: Uuid) {
let render_state = get_render_state();
// We don't really do a self.shapes.remove so that redo/undo keep working
let Some(shape) = self.shapes.get(&id) else {
return;
@ -155,16 +137,15 @@ impl State {
//
// Instead, remove the shape from *all* tiles where it was indexed, and
// drop cached tiles for those entries.
let indexed_tiles: Vec<tiles::Tile> = self
.render_state
let indexed_tiles: Vec<tiles::Tile> = render_state
.tiles
.get_tiles_of(shape.id)
.map(|t| t.iter().copied().collect())
.unwrap_or_default();
for tile in indexed_tiles {
self.render_state.remove_cached_tile(tile);
self.render_state.tiles.remove_shape_at(tile, shape.id);
render_state.remove_cached_tile(tile);
render_state.tiles.remove_shape_at(tile, shape.id);
}
if let Some(shape_to_delete) = self.shapes.get(&id) {
@ -173,8 +154,8 @@ impl State {
if let Some(shape_to_delete) = self.shapes.get_mut(&shape_id) {
shape_to_delete.set_deleted(true);
}
if self.render_state.show_grid == Some(shape_id) {
self.render_state.show_grid = None;
if render_state.show_grid == Some(shape_id) {
render_state.show_grid = None;
}
}
}
@ -190,7 +171,7 @@ impl State {
}
pub fn set_background_color(&mut self, color: skia::Color) {
self.render_state.set_background_color(color);
get_render_state().set_background_color(color);
}
pub fn set_browser(&mut self, browser: u8) {
@ -225,33 +206,32 @@ impl State {
}
pub fn rebuild_tiles_shallow(&mut self) {
self.render_state.rebuild_tiles_shallow(&self.shapes);
get_render_state().rebuild_tiles_shallow(&self.shapes);
}
pub fn rebuild_tiles(&mut self) {
self.render_state.rebuild_tiles_from(&self.shapes, None);
get_render_state().rebuild_tiles_from(&self.shapes, None);
}
pub fn rebuild_tiles_from(&mut self, base_id: Option<&Uuid>) {
self.render_state.rebuild_tiles_from(&self.shapes, base_id);
get_render_state().rebuild_tiles_from(&self.shapes, base_id);
}
pub fn rebuild_touched_tiles(&mut self) {
self.render_state.rebuild_touched_tiles(&self.shapes);
get_render_state().rebuild_touched_tiles(&self.shapes);
}
pub fn render_preview(&mut self, timestamp: i32) {
let _ = self.render_state.render_preview(&self.shapes, timestamp);
let _ = get_render_state().render_preview(&self.shapes, timestamp);
}
pub fn rebuild_modifier_tiles(&mut self, ids: Vec<Uuid>) -> Result<()> {
// Index-based storage is safe
self.render_state
.rebuild_modifier_tiles(&mut self.shapes, ids)
get_render_state().rebuild_modifier_tiles(&mut self.shapes, ids)
}
pub fn font_collection(&self) -> &FontCollection {
self.render_state.fonts().font_collection()
get_render_state().fonts().font_collection()
}
pub fn get_grid_coords(&self, pos_x: f32, pos_y: f32) -> Option<(i32, i32)> {
@ -284,16 +264,18 @@ impl State {
}
pub fn touch_current(&mut self) {
let render_state = get_render_state();
if !self.loading {
if let Some(current_id) = self.current_id {
self.render_state.mark_touched(current_id);
render_state.mark_touched(current_id);
}
}
}
pub fn touch_shape(&mut self, id: Uuid) {
let render_state = get_render_state();
if !self.loading {
self.render_state.mark_touched(id);
render_state.mark_touched(id);
}
}
}

View File

@ -1,3 +1,4 @@
use crate::get_render_state;
use crate::skia::textlayout::FontCollection;
use crate::skia::Image;
use crate::uuid::Uuid;
@ -25,12 +26,12 @@ pub fn uuid_from_u32(id: [u32; 4]) -> Uuid {
}
pub fn get_image(image_id: &Uuid) -> Option<&Image> {
with_state_mut!(state, { state.render_state_mut().images.get(image_id) })
get_render_state().images.get(image_id)
}
// FIXME: move to a different place ?
pub fn get_fallback_fonts() -> &'static HashSet<String> {
with_state_mut!(state, { state.render_state().fonts().get_fallback() })
get_render_state().fonts().get_fallback()
}
pub fn get_font_collection() -> &'static FontCollection {

View File

@ -1,4 +1,5 @@
use crate::error::{Error, Result};
use crate::get_render_state;
use crate::mem;
use crate::shapes::Fill;
use crate::state::State;
@ -106,11 +107,7 @@ pub extern "C" fn store_image() -> Result<()> {
let image_bytes = &bytes[IMAGE_HEADER_SIZE..];
with_state_mut!(state, {
if let Err(msg) =
state
.render_state_mut()
.add_image(ids.image_id, is_thumbnail, image_bytes)
{
if let Err(msg) = get_render_state().add_image(ids.image_id, is_thumbnail, image_bytes) {
eprintln!("{}", msg);
}
touch_shapes_with_image(state, ids.image_id);
@ -180,7 +177,7 @@ pub extern "C" fn store_image_from_texture() -> Result<()> {
);
with_state_mut!(state, {
if let Err(msg) = state.render_state_mut().add_image_from_gl_texture(
if let Err(msg) = get_render_state().add_image_from_gl_texture(
ids.image_id,
is_thumbnail,
texture_id,

View File

@ -1,10 +1,9 @@
use macros::{wasm_error, ToJs};
use crate::get_render_state;
use crate::mem;
use crate::shapes::{FontFamily, FontStyle};
use crate::utils::uuid_from_u32_quartet;
use crate::with_state_mut;
use crate::STATE;
#[derive(Debug, PartialEq, Clone, Copy, ToJs)]
#[repr(u8)]
@ -41,20 +40,16 @@ pub extern "C" fn store_font(
is_emoji: bool,
is_fallback: bool,
) -> Result<()> {
with_state_mut!(state, {
let id = uuid_from_u32_quartet(a, b, c, d);
let font_bytes = mem::bytes();
let font_style = RawFontStyle::from(style);
let id = uuid_from_u32_quartet(a, b, c, d);
let font_bytes = mem::bytes();
let font_style = RawFontStyle::from(style);
let family = FontFamily::new(id, weight, font_style.into());
let _ =
state
.render_state_mut()
.fonts_mut()
.add(family, &font_bytes, is_emoji, is_fallback);
let family = FontFamily::new(id, weight, font_style.into());
let _ = get_render_state()
.fonts_mut()
.add(family, &font_bytes, is_emoji, is_fallback);
mem::free_bytes()?;
});
mem::free_bytes()?;
Ok(())
}
@ -68,12 +63,10 @@ pub extern "C" fn is_font_uploaded(
style: u8,
is_emoji: bool,
) -> bool {
with_state_mut!(state, {
let id = uuid_from_u32_quartet(a, b, c, d);
let font_style = RawFontStyle::from(style);
let family = FontFamily::new(id, weight, font_style.into());
let res = state.render_state().fonts().has_family(&family, is_emoji);
let id = uuid_from_u32_quartet(a, b, c, d);
let font_style = RawFontStyle::from(style);
let family = FontFamily::new(id, weight, font_style.into());
let res = get_render_state().fonts().has_family(&family, is_emoji);
res
})
res
}

View File

@ -1,9 +1,10 @@
use macros::{wasm_error, ToJs};
use crate::get_render_state;
use crate::mem;
use crate::shapes::{GridCell, GridDirection, GridTrack, GridTrackType};
use crate::uuid::Uuid;
use crate::{uuid_from_u32_quartet, with_current_shape_mut, with_state, with_state_mut, STATE};
use crate::{uuid_from_u32_quartet, with_current_shape_mut, with_state, STATE};
use super::align;
@ -241,17 +242,13 @@ pub extern "C" fn set_grid_cells() -> Result<()> {
#[no_mangle]
pub extern "C" fn show_grid(a: u32, b: u32, c: u32, d: u32) {
with_state_mut!(state, {
let id = uuid_from_u32_quartet(a, b, c, d);
state.render_state.show_grid = Some(id);
});
let id = uuid_from_u32_quartet(a, b, c, d);
get_render_state().show_grid = Some(id);
}
#[no_mangle]
pub extern "C" fn hide_grid() {
with_state_mut!(state, {
state.render_state.show_grid = None;
});
get_render_state().show_grid = None;
}
#[no_mangle]

View File

@ -2,7 +2,6 @@ use macros::{wasm_error, ToJs};
use crate::get_text_editor_state;
use crate::math::{Matrix, Point, Rect};
use crate::mem;
use crate::render::text_editor as text_editor_render;
use crate::render::SurfaceId;
use crate::shapes::{Shape, TextAlign, TextContent, TextPositionWithAffinity, Type, VerticalAlign};
@ -13,6 +12,7 @@ use crate::wasm::fills::RawFillData;
use crate::wasm::text::{
helpers as text_helpers, RawTextAlign, RawTextDecoration, RawTextDirection, RawTextTransform,
};
use crate::{get_render_state, mem};
use crate::{with_state, with_state_mut, STATE};
use skia_safe::Color;
@ -287,7 +287,7 @@ pub extern "C" fn text_editor_set_cursor_from_point(x: f32, y: f32) {
return;
}
let view_matrix: Matrix = state.render_state.viewbox.get_matrix();
let view_matrix: Matrix = get_render_state().viewbox.get_matrix();
let point = Point::new(x, y);
let Some(shape_id) = get_text_editor_state().active_shape_id else {
return;
@ -368,7 +368,7 @@ pub extern "C" fn text_editor_composition_end() -> Result<()> {
get_text_editor_state().push_event(crate::state::TextEditorEvent::ContentChanged);
get_text_editor_state().push_event(crate::state::TextEditorEvent::NeedsLayout);
state.render_state.mark_touched(shape_id);
get_render_state().mark_touched(shape_id);
get_text_editor_state().composition.end();
});
@ -420,7 +420,7 @@ pub extern "C" fn text_editor_composition_update() -> Result<()> {
get_text_editor_state().push_event(crate::state::TextEditorEvent::ContentChanged);
get_text_editor_state().push_event(crate::state::TextEditorEvent::NeedsLayout);
state.render_state.mark_touched(shape_id);
get_render_state().mark_touched(shape_id);
});
crate::mem::free_bytes()?;
@ -489,7 +489,7 @@ pub extern "C" fn text_editor_insert_text() -> Result<()> {
get_text_editor_state().push_event(TextEditorEvent::ContentChanged);
get_text_editor_state().push_event(TextEditorEvent::NeedsLayout);
state.render_state.mark_touched(shape_id);
get_render_state().mark_touched(shape_id);
});
crate::mem::free_bytes()?;
@ -516,7 +516,7 @@ pub extern "C" fn text_editor_delete_backward(word_boundary: bool) {
};
get_text_editor_state().delete_backward(text_content, word_boundary);
state.render_state.mark_touched(shape_id);
get_render_state().mark_touched(shape_id);
});
}
@ -540,7 +540,7 @@ pub extern "C" fn text_editor_delete_forward(word_boundary: bool) {
};
get_text_editor_state().delete_forward(text_content, word_boundary);
state.render_state.mark_touched(shape_id);
get_render_state().mark_touched(shape_id);
});
}
@ -564,7 +564,7 @@ pub extern "C" fn text_editor_insert_paragraph() {
};
get_text_editor_state().insert_paragraph(text_content);
state.render_state.mark_touched(shape_id);
get_render_state().mark_touched(shape_id);
});
}
@ -880,16 +880,16 @@ pub extern "C" fn text_editor_render_overlay() {
return;
};
let canvas = state.render_state.surfaces.canvas(SurfaceId::Target);
let viewbox = state.render_state.viewbox;
let canvas = get_render_state().surfaces.canvas(SurfaceId::Target);
let viewbox = get_render_state().viewbox;
text_editor_render::render_overlay(
canvas,
&viewbox,
&state.render_state.options,
&get_render_state().options,
get_text_editor_state(),
shape,
);
state.render_state.flush_and_submit();
get_render_state().flush_and_submit();
});
}