Compare commits

...

21 Commits

Author SHA1 Message Date
Aitor Moreno
dbbf3ab41d WIP 2025-07-24 10:16:48 +02:00
Aitor Moreno
5b26a9a269 WIP 2025-07-24 09:23:19 +02:00
Aitor Moreno
f56420d327 WIP 2025-07-24 09:23:19 +02:00
Aitor Moreno
462ac77fcd WIP 2025-07-24 09:23:19 +02:00
Aitor Moreno
fd3c3d720e WIP 2025-07-24 09:23:19 +02:00
Aitor Moreno
a20e0c5733 WIP 2025-07-24 09:23:19 +02:00
Aitor Moreno
26ca7ed566 WIP 2025-07-24 09:23:19 +02:00
Aitor Moreno
2a58387b02 WIP 2025-07-24 09:23:19 +02:00
Aitor Moreno
84ad14183e WIP 2025-07-24 09:23:19 +02:00
Aitor Moreno
0704fa5df6 WIP 2025-07-24 09:23:19 +02:00
Aitor Moreno
b438f4dfa1 WIP 2025-07-24 09:23:19 +02:00
Aitor Moreno
4a85a9ac46 WIP 2025-07-24 09:23:19 +02:00
Aitor Moreno
a77ca32bf8 WIP 2025-07-24 09:23:19 +02:00
Aitor Moreno
2a7ab93892 WIP 2025-07-24 09:23:19 +02:00
Aitor Moreno
03e4ff828b WIP 2025-07-24 09:23:19 +02:00
Alejandro Alonso
2ab5487a84 WIP fix partial 2025-07-24 09:23:19 +02:00
Aitor Moreno
f60254e3eb WIP 2025-07-24 09:23:19 +02:00
Aitor Moreno
4274db11e3 WIP 2025-07-24 09:23:19 +02:00
Aitor Moreno
ee3bf72034 WIP 2025-07-24 09:23:19 +02:00
Aitor Moreno
446fc53f00 WIP 2025-07-24 09:23:19 +02:00
Aitor Moreno
3af70a965d WIP 2025-07-24 09:23:19 +02:00
12 changed files with 617 additions and 198 deletions

View File

@@ -29,33 +29,33 @@
const iterations = parseInt(url.searchParams.get('iterations') ?? 1_000, 10);
function prepare(Module, canvas) {
init(Module);
assignCanvas(canvas);
Module._set_canvas_background(hexToU32ARGB("#FABADA", 1));
Module._set_view(1, 0, 0);
setup({
instance: Module,
canvas
})
const children = [];
for (let shape = 0; shape < shapes; shape++) {
const uuid = crypto.randomUUID();
children.push(uuid);
useShape(uuid);
Module._set_parent(0, 0, 0, 0);
Module._set_shape_type(3);
const x1 = getRandomInt(0, canvas.width);
const y1 = getRandomInt(0, canvas.height);
const width = getRandomInt(20, 100);
const height = getRandomInt(20, 100);
Module._set_shape_selrect(x1, y1, x1 + width, y1 + height);
const color = getRandomColor();
const argb = hexToU32ARGB(color, getRandomFloat(0.1, 1.0));
addShapeSolidFill(argb)
children.push(
addShape({
parent: "00000000-0000-0000-0000-000000000000",
type: "rect",
selrect: {
x: getRandomInt(0, canvas.width),
y: getRandomInt(0, canvas.height),
width: getRandomInt(20, 100),
height: getRandomInt(20, 100)
},
fills: [{ type: "solid", color: getRandomColor(), opacity: getRandomFloat(0.1, 1.0) }]
})
);
}
useShape("00000000-0000-0000-0000-000000000000");
setShapeChildren(children);
addShape({
id: "00000000-0000-0000-0000-000000000000",
children
})
}
function createElement(tag, attribs, children) {

View File

@@ -27,12 +27,12 @@ export function assignCanvas(canvas) {
context.getExtension("WEBGL_debug_renderer_info");
Module._init(canvas.width, canvas.height);
Module._set_render_options(0, 1);
Module._set_render_options(1, 1);
}
export function hexToU32ARGB(hex, opacity = 1) {
const rgb = parseInt(hex.slice(1), 16);
const a = Math.floor(opacity * 0xFF);
const a = Math.floor(opacity * 0xff);
const argb = (a << 24) | rgb;
return argb >>> 0;
}
@@ -42,9 +42,9 @@ export function getRandomInt(min, max) {
}
export function getRandomColor() {
const r = getRandomInt(0, 256).toString(16).padStart(2, '0');
const g = getRandomInt(0, 256).toString(16).padStart(2, '0');
const b = getRandomInt(0, 256).toString(16).padStart(2, '0');
const r = getRandomInt(0, 256).toString(16).padStart(2, "0");
const g = getRandomInt(0, 256).toString(16).padStart(2, "0");
const b = getRandomInt(0, 256).toString(16).padStart(2, "0");
return `#${r}${g}${b}`;
}
@@ -103,12 +103,12 @@ export function addShapeSolidStrokeFill(argb) {
function serializePathAttrs(svgAttrs) {
return Object.entries(svgAttrs).reduce((acc, [key, value]) => {
return acc + key + '\0' + value + '\0';
}, '');
return acc + key + "\0" + value + "\0";
}, "");
}
export function draw_star(x, y, width, height) {
const len = 11; // 1 MOVE + 9 LINE + 1 CLOSE
export function drawStar(x, y, width, height) {
const len = 11; // 1 MOVE + 9 LINE + 1 CLOSE
const ptr = allocBytes(len * 28);
const heap = getHeapU32();
const dv = new DataView(heap.buffer);
@@ -120,7 +120,7 @@ export function draw_star(x, y, width, height) {
const star = [];
for (let i = 0; i < 10; i++) {
const angle = Math.PI / 5 * i - Math.PI / 2;
const angle = (Math.PI / 5) * i - Math.PI / 2;
const r = i % 2 === 0 ? outerRadius : innerRadius;
const px = cx + r * Math.cos(angle);
const py = cy + r * Math.sin(angle);
@@ -149,7 +149,7 @@ export function draw_star(x, y, width, height) {
Module._set_shape_path_content();
const str = serializePathAttrs({
"fill": "none",
fill: "none",
"stroke-linecap": "round",
"stroke-linejoin": "round",
});
@@ -158,7 +158,6 @@ export function draw_star(x, y, width, height) {
Module.stringToUTF8(str, offset, size);
Module._set_shape_path_attrs(3);
}
export function setShapeChildren(shapeIds) {
const offset = allocBytes(shapeIds.length * 16);
@@ -176,7 +175,7 @@ export function useShape(id) {
Module._use_shape(...buffer);
}
export function set_parent(id) {
export function setParent(id) {
const buffer = getU32(id);
Module._set_parent(...buffer);
}
@@ -227,8 +226,12 @@ export function setupInteraction(canvas) {
}
});
canvas.addEventListener("mouseup", () => { isPanning = false; });
canvas.addEventListener("mouseout", () => { isPanning = false; });
canvas.addEventListener("mouseup", () => {
isPanning = false;
});
canvas.addEventListener("mouseout", () => {
isPanning = false;
});
}
export function addTextShape(x, y, fontSize, text) {
@@ -312,4 +315,57 @@ export function addTextShape(x, y, fontSize, text) {
// Call the WebAssembly function
Module._set_shape_text_content();
}
}
export function setup(options) {
init(options.instance)
assignCanvas(options.canvas)
Module._set_canvas_background(hexToU32ARGB(options?.backgroundColor ?? "#FABADA", 1));
Module._set_view(options?.zoom ?? 1, options?.x ?? 0, options?.y ?? 0);
Module._init_shapes_pool(options.shapes + 1);
setupInteraction(options.canvas);
}
function getShapeType(type) {
switch (type) {
default:
case "rect": return 3;
}
}
export function addShape(init) {
const uuid = init?.id ?? crypto.randomUUID()
useShape(uuid);
setParent(init?.parent ?? "00000000-0000-0000-0000-000000000000");
Module._set_shape_type(getShapeType(init?.type));
if (init.selrect) {
Module._set_shape_selrect(
init.selrect.x,
init.selrect.y,
init.selrect.x + init.selrect.width,
init.selrect.y + init.selrect.height,
);
}
if (Array.isArray(init?.fills)) {
for (const fill of init.fills) {
const argb = hexToU32ARGB(fill.color, fill.opacity);
addShapeSolidFill(argb);
}
}
if (Array.isArray(init?.strokes)) {
for (const stroke of init.strokes) {
Module._add_shape_center_stroke(stroke.width, 0, 0, 0);
const argb = hexToU32ARGB(stroke.color, stroke.opacity);
addShapeSolidStrokeFill(argb);
}
}
if (Array.isArray(init?.children)) {
setShapeChildren(init.children);
}
return uuid
}

View File

@@ -25,8 +25,8 @@
<script type="module">
import initWasmModule from '/js/render_wasm.js';
import {
init, addShapeSolidFill, assignCanvas, hexToU32ARGB, getRandomInt, getRandomColor,
getRandomFloat, useShape, setShapeChildren, setupInteraction, addShapeSolidStrokeFill
addShapeSolidFill, hexToU32ARGB, getRandomInt, getRandomColor,
getRandomFloat, addShape, setShapeChildren, setup
} from './js/lib.js';
const canvas = document.getElementById("canvas");
@@ -37,46 +37,43 @@
const shapes = params.get("shapes") || 1000;
initWasmModule().then(Module => {
init(Module);
assignCanvas(canvas);
Module._set_canvas_background(hexToU32ARGB("#FABADA", 1));
Module._set_view(1, 0, 0);
Module._init_shapes_pool(shapes + 1);
setupInteraction(canvas);
setup({
instance: Module,
canvas,
shapes
})
const children = [];
for (let i = 0; i < shapes; i++) {
const uuid = crypto.randomUUID();
children.push(uuid);
useShape(uuid);
Module._set_parent(0, 0, 0, 0);
Module._set_shape_type(3);
const x1 = getRandomInt(0, canvas.width);
const y1 = getRandomInt(0, canvas.height);
const width = getRandomInt(20, 100);
const height = getRandomInt(20, 100);
Module._set_shape_selrect(x1, y1, x1 + width, y1 + height);
const color = getRandomColor();
const argb = hexToU32ARGB(color, getRandomFloat(0.1, 1.0));
addShapeSolidFill(argb)
Module._add_shape_center_stroke(10, 0, 0, 0);
const argb2 = hexToU32ARGB(color, getRandomFloat(0.1, 1.0));
addShapeSolidStrokeFill(argb2);
for (let shape = 0; shape < shapes; shape++) {
const color = getRandomColor()
children.push(
addShape({
parent: "00000000-0000-0000-0000-000000000000",
type: "rect", // rect
selrect: {
x: getRandomInt(0, canvas.width),
y: getRandomInt(0, canvas.height),
width: getRandomInt(20, 100),
height: getRandomInt(20, 100),
},
fills: [{ type: "solid", color, opacity: getRandomFloat(0.1, 1.0) }],
strokes: [{ width: 10, type: "solid", color, opacity: getRandomFloat(0.1, 1.0) }]
})
);
}
useShape("00000000-0000-0000-0000-000000000000");
setShapeChildren(children);
addShape({
id: "00000000-0000-0000-0000-000000000000",
children: children
})
performance.mark('render:begin');
Module._render(Date.now());
Module._render(performance.now(), true);
performance.mark('render:end');
const { duration } = performance.measure('render', 'render:begin', 'render:end');
// alert(`render time: ${duration.toFixed(2)}ms`);
});
</script>
</body>
</html>

View File

@@ -98,9 +98,15 @@
;; This should never be called from the outside.
(defn- render
[timestamp]
(h/call wasm/internal-module "_render" timestamp)
(set! wasm/internal-frame-id nil))
([]
(render (.now js/performance)))
([timestamp]
(render timestamp false))
([timestamp full]
(h/call wasm/internal-module "_render" timestamp full)
(set! wasm/internal-frame-id nil)))
(def debounce-render (fns/debounce render 100))

View File

@@ -2,7 +2,7 @@
macro_rules! run_script {
($s:expr) => {{
extern "C" {
pub fn emscripten_run_script(script: *const i8);
fn emscripten_run_script(script: *const i8);
}
match std::ffi::CString::new($s) {
@@ -16,7 +16,7 @@ macro_rules! run_script {
macro_rules! run_script_int {
($s:expr) => {{
extern "C" {
pub fn emscripten_run_script_int(script: *const i8) -> i32;
fn emscripten_run_script_int(script: *const i8) -> i32;
}
match std::ffi::CString::new($s) {
@@ -30,7 +30,7 @@ macro_rules! run_script_int {
macro_rules! get_now {
() => {{
extern "C" {
pub fn emscripten_get_now() -> f64;
fn emscripten_get_now() -> f64;
}
unsafe { emscripten_get_now() }
}};
@@ -54,6 +54,54 @@ macro_rules! init_gl {
}};
}
#[macro_export]
macro_rules! debugger {
() => {{
extern "C" {
fn emscripten_debugger();
}
unsafe { emscripten_debugger() }
}}
}
#[repr(u32)]
#[allow(dead_code)]
pub enum Log {
Default = 1,
Warn = 2,
Error = 4,
CStack = 8,
JSStack = 16,
Demangle = 32,
NoPaths = 64,
FuncParams = 128,
}
#[macro_export]
macro_rules! log {
($flags:expr, $($arg:tt)*) => {{
#[cfg(debug_assertions)]
{
extern "C" {
fn emscripten_log(flags: u32, message: *const std::os::raw::c_char);
}
use std::ffi::CString;
let msg = format!($($arg)*);
let c_msg = CString::new(msg).unwrap();
let flags = $flags as u32;
unsafe {
emscripten_log(flags, c_msg.as_ptr());
}
}
}}
}
#[allow(unused_imports)]
pub use log;
#[allow(unused_imports)]
pub use debugger;
#[allow(unused_imports)]
pub use run_script;
@@ -65,3 +113,4 @@ pub use get_now;
#[allow(unused_imports)]
pub use init_gl;

View File

@@ -1,4 +1,3 @@
#[cfg(target_arch = "wasm32")]
mod emscripten;
mod math;
mod mem;
@@ -119,10 +118,10 @@ pub extern "C" fn set_canvas_background(raw_color: u32) {
}
#[no_mangle]
pub extern "C" fn render(_: i32) {
pub extern "C" fn render(_: i32, full: bool) {
with_state_mut!(state, {
state
.start_render_loop(performance::get_time())
.start_render_loop(performance::get_time(), full)
.expect("Error rendering");
});
}

View File

@@ -12,14 +12,15 @@ mod surfaces;
mod text;
mod ui;
use skia_safe::{self as skia, Matrix, Rect};
use skia_safe::{self as skia, Color, Matrix, Rect};
use std::borrow::Cow;
use std::collections::{HashMap, HashSet};
use gpu_state::GpuState;
use options::RenderOptions;
use options::RenderStateOptions;
use surfaces::{SurfaceId, Surfaces};
use crate::emscripten;
use crate::performance;
use crate::shapes::{Corners, Fill, Shape, SolidColor, StructureEntry, Type};
use crate::state::ShapesPool;
@@ -37,6 +38,104 @@ const VIEWPORT_INTEREST_AREA_THRESHOLD: i32 = 1;
const MAX_BLOCKING_TIME_MS: i32 = 32;
const NODE_BATCH_THRESHOLD: i32 = 10;
pub struct PendingNodes {
pub list: Vec<NodeRenderState>,
}
impl PendingNodes {
pub fn new_empty() -> Self {
Self { list: vec![] }
}
pub fn is_empty(&self) -> bool {
self.list.is_empty()
}
pub fn len(&self) -> usize {
self.list.len()
}
pub fn extend(&mut self, valid_ids: Vec<Uuid>) {
self.list
.extend(valid_ids.into_iter().map(|id| NodeRenderState {
id,
visited_children: false,
clip_bounds: None,
visited_mask: false,
mask: false,
}));
}
pub fn next(&mut self) -> Option<NodeRenderState> {
self.list.pop()
}
pub fn prepare(&mut self, tree: &ShapesPool) {
self.list.clear();
if self.list.capacity() < tree.len() {
self.list.reserve(tree.len() - self.list.capacity());
}
}
pub fn add_root(&mut self) {
emscripten::log!(emscripten::Log::Default, "add_root");
self.list.push(NodeRenderState {
id: Uuid::nil(),
visited_children: false,
clip_bounds: None,
visited_mask: false,
mask: false,
});
}
pub fn add_child(
&mut self,
child_id: &Uuid,
children_clip_bounds: Option<(Rect, Option<Corners>, Matrix)>,
) {
emscripten::log!(emscripten::Log::Default, "add_child");
self.list.push(NodeRenderState {
id: *child_id,
visited_children: false,
clip_bounds: children_clip_bounds,
visited_mask: false,
mask: false,
});
}
pub fn add_children_safeguard(&mut self, element: &Shape, mask: bool) {
emscripten::log!(emscripten::Log::Default, "add_children_safeguard");
// Set the node as visited_children before processing children
self.list.push(NodeRenderState {
id: element.id,
visited_children: true,
clip_bounds: None,
visited_mask: false,
mask,
});
}
pub fn add_mask(&mut self, element: &Shape) {
emscripten::log!(emscripten::Log::Default, "add_mask");
self.list.push(NodeRenderState {
id: element.id,
visited_children: true,
clip_bounds: None,
visited_mask: true,
mask: false,
});
if let Some(&mask_id) = element.mask_id() {
self.list.push(NodeRenderState {
id: mask_id,
visited_children: false,
clip_bounds: None,
visited_mask: false,
mask: true,
});
}
}
}
pub struct NodeRenderState {
pub id: Uuid,
// We use this bool to keep that we've traversed all the children inside this node.
@@ -151,7 +250,7 @@ impl FocusMode {
pub(crate) struct RenderState {
gpu_state: GpuState,
pub options: RenderOptions,
pub options: RenderStateOptions,
pub surfaces: Surfaces,
pub fonts: FontStore,
pub viewbox: Viewbox,
@@ -163,8 +262,9 @@ pub(crate) struct RenderState {
pub render_request_id: Option<i32>,
// Indicates whether the rendering process has pending frames.
pub render_in_progress: bool,
pub render_is_full: bool,
// Stack of nodes pending to be rendered.
pending_nodes: Vec<NodeRenderState>,
pending_nodes: PendingNodes,
pub current_tile: Option<tiles::Tile>,
pub sampling_options: skia::SamplingOptions,
pub render_area: Rect,
@@ -220,19 +320,22 @@ impl RenderState {
let viewbox = Viewbox::new(width as f32, height as f32);
let tiles = tiles::TileHashMap::new();
let context = gpu_state.context.clone();
RenderState {
gpu_state: gpu_state.clone(),
options: RenderOptions::default(),
gpu_state,
options: RenderStateOptions::default(),
surfaces,
fonts,
viewbox,
cached_viewbox: Viewbox::new(0., 0.),
cached_target_snapshot: None,
images: ImageStore::new(gpu_state.context.clone()),
images: ImageStore::new(context),
background_color: skia::Color::TRANSPARENT,
render_request_id: None,
render_in_progress: false,
pending_nodes: vec![],
render_is_full: false,
pending_nodes: PendingNodes::new_empty(),
current_tile: None,
sampling_options,
render_area: Rect::new_empty(),
@@ -312,6 +415,11 @@ impl RenderState {
self.surfaces.canvas(surface_id).restore();
}
pub fn apply_full_render_to_final_canvas(&mut self) {
emscripten::log!(emscripten::Log::Default, "apply_full_render_to_final_canvas");
self.surfaces.draw_into(SurfaceId::Full, SurfaceId::Target, None);
}
pub fn apply_render_to_final_canvas(&mut self, rect: skia::Rect) {
let tile_rect = self.get_current_aligned_tile_bounds();
self.surfaces.cache_current_tile_texture(
@@ -336,6 +444,58 @@ impl RenderState {
}
}
pub fn apply_drawing_to_full_render_canvas(&mut self, shape: Option<&Shape>) {
performance::begin_measure!("apply_drawing_to_full_render_canvas");
self.surfaces.draw_into(
SurfaceId::DropShadows,
SurfaceId::Full,
Some(&skia::Paint::default()),
);
self.surfaces.draw_into(
SurfaceId::Fills,
SurfaceId::Full,
Some(&skia::Paint::default()),
);
let mut render_overlay_below_strokes = false;
if let Some(shape) = shape {
render_overlay_below_strokes = shape.has_fills();
}
if render_overlay_below_strokes {
self.surfaces.draw_into(
SurfaceId::InnerShadows,
SurfaceId::Full,
Some(&skia::Paint::default()),
);
}
self.surfaces.draw_into(
SurfaceId::Strokes,
SurfaceId::Full,
Some(&skia::Paint::default()),
);
if !render_overlay_below_strokes {
self.surfaces.draw_into(
SurfaceId::InnerShadows,
SurfaceId::Full,
Some(&skia::Paint::default()),
);
}
let surface_ids = SurfaceId::Strokes as u32
| SurfaceId::Fills as u32
| SurfaceId::DropShadows as u32
| SurfaceId::InnerShadows as u32;
self.surfaces.apply_mut(surface_ids, |s| {
s.canvas().clear(skia::Color::TRANSPARENT);
});
performance::end_measure!("apply_drawing_to_full_render_canvas");
}
pub fn apply_drawing_to_render_canvas(&mut self, shape: Option<&Shape>) {
performance::begin_measure!("apply_drawing_to_render_canvas");
@@ -385,6 +545,8 @@ impl RenderState {
self.surfaces.apply_mut(surface_ids, |s| {
s.canvas().clear(skia::Color::TRANSPARENT);
});
performance::end_measure!("apply_drawing_to_render_canvas");
}
pub fn clear_focus_mode(&mut self) {
@@ -401,6 +563,7 @@ impl RenderState {
modifiers: Option<&Matrix>,
scale_content: Option<&f32>,
) {
emscripten::log!(emscripten::Log::Default, "render_shape {}", shape.id);
let shape = if let Some(scale_content) = scale_content {
&shape.scale_content(*scale_content)
} else {
@@ -412,6 +575,7 @@ impl RenderState {
| SurfaceId::DropShadows as u32
| SurfaceId::InnerShadows as u32;
self.surfaces.apply_mut(surface_ids, |s| {
emscripten::log!(emscripten::Log::Default, "render_shape::save");
s.canvas().save();
});
@@ -432,8 +596,10 @@ impl RenderState {
match &shape.shape_type {
Type::SVGRaw(sr) => {
if let Some(modifiers) = modifiers {
emscripten::log!(emscripten::Log::Default, "render_shape::concat (modifiers)");
self.surfaces.canvas(SurfaceId::Fills).concat(modifiers);
}
emscripten::log!(emscripten::Log::Default, "render_shape::concat");
self.surfaces.canvas(SurfaceId::Fills).concat(&matrix);
if let Some(svg) = shape.svg.as_ref() {
svg.render(self.surfaces.canvas(SurfaceId::Fills))
@@ -458,6 +624,7 @@ impl RenderState {
| SurfaceId::DropShadows as u32
| SurfaceId::InnerShadows as u32;
self.surfaces.apply_mut(surface_ids, |s| {
emscripten::log!(emscripten::Log::Default, "render_shape::concat");
s.canvas().concat(&matrix);
});
@@ -517,6 +684,7 @@ impl RenderState {
| SurfaceId::DropShadows as u32
| SurfaceId::InnerShadows as u32;
self.surfaces.apply_mut(surface_ids, |s| {
emscripten::log!(emscripten::Log::Default, "render_shape::concat");
s.canvas().concat(&matrix);
});
@@ -557,19 +725,25 @@ impl RenderState {
| SurfaceId::DropShadows as u32
| SurfaceId::InnerShadows as u32;
self.surfaces.apply_mut(surface_ids, |s| {
emscripten::log!(emscripten::Log::Default, "render_shape::restore");
s.canvas().restore();
});
}
pub fn update_render_context(&mut self, tile: tiles::Tile) {
self.current_tile = Some(tile);
self.render_area = tiles::get_tile_rect(tile, self.get_scale());
pub fn get_tiles_of(&mut self, id: Uuid) -> Option<&HashSet<tiles::Tile>> {
self.tiles.get_tiles_of(id)
}
pub fn update_render_context(&mut self, tile: &tiles::Tile) {
self.current_tile = Some(*tile);
self.render_area = tiles::get_tile_rect(*tile, self.get_scale());
self.surfaces
.update_render_context(self.render_area, self.get_scale());
}
pub fn cancel_animation_frame(&mut self) {
if self.render_in_progress {
// self.render_in_progress = false;
if let Some(frame_id) = self.render_request_id {
wapi::cancel_animation_frame!(frame_id);
}
@@ -624,6 +798,7 @@ impl RenderState {
structure: &HashMap<Uuid, Vec<StructureEntry>>,
scale_content: &HashMap<Uuid, f32>,
timestamp: i32,
full: bool,
) -> Result<(), String> {
let scale = self.get_scale();
self.tile_viewbox.update(self.viewbox, scale);
@@ -658,14 +833,16 @@ impl RenderState {
self.pending_tiles.update(&self.tile_viewbox);
performance::end_measure!("tile_cache");
self.pending_nodes.clear();
if self.pending_nodes.capacity() < tree.len() {
self.pending_nodes
.reserve(tree.len() - self.pending_nodes.capacity());
}
// reorder by distance to the center.
self.pending_nodes.prepare(tree);
self.current_tile = None;
self.render_in_progress = true;
self.render_is_full = full;
if self.render_is_full {
emscripten::log!(emscripten::Log::Default, "pending_nodes.add_root");
self.pending_nodes.add_root();
}
self.apply_drawing_to_render_canvas(None);
self.process_animation_frame(tree, modifiers, structure, scale_content, timestamp)?;
performance::end_measure!("start_render_loop");
@@ -682,14 +859,23 @@ impl RenderState {
) -> Result<(), String> {
performance::begin_measure!("process_animation_frame");
if self.render_in_progress {
self.render_shape_tree_partial(tree, modifiers, structure, scale_content, timestamp)?;
if self.render_is_full {
self.render_shape_tree_full(tree, modifiers, structure, scale_content, timestamp)?;
} else {
self.render_shape_tree_partial(
tree,
modifiers,
structure,
scale_content,
timestamp,
)?;
}
self.flush_and_submit();
if self.render_in_progress {
self.cancel_animation_frame();
self.render_request_id = Some(wapi::request_animation_frame!());
} else {
performance::end_measure!("render");
}
}
performance::end_measure!("process_animation_frame");
@@ -698,8 +884,23 @@ impl RenderState {
#[inline]
pub fn should_stop_rendering(&self, iteration: i32, timestamp: i32) -> bool {
iteration % NODE_BATCH_THRESHOLD == 0
&& performance::get_time() - timestamp > MAX_BLOCKING_TIME_MS
self.pending_nodes.is_empty() ||
(iteration % NODE_BATCH_THRESHOLD == 0
&& performance::get_time() - timestamp > MAX_BLOCKING_TIME_MS)
}
#[inline]
pub fn render_current_tile_to_final_canvas(&mut self, is_empty: bool) {
let tile_rect = self.get_current_tile_bounds();
if !is_empty {
self.apply_render_to_final_canvas(tile_rect);
} else {
self.surfaces.apply_mut(SurfaceId::Target as u32, |s| {
let mut paint = skia::Paint::default();
paint.set_color(self.background_color);
s.canvas().draw_rect(tile_rect, &paint);
});
}
}
#[inline]
@@ -774,22 +975,7 @@ impl RenderState {
// the blend mode 'destination-in') the content
// of the group and the mask.
if group.masked {
self.pending_nodes.push(NodeRenderState {
id: element.id,
visited_children: true,
clip_bounds: None,
visited_mask: true,
mask: false,
});
if let Some(&mask_id) = element.mask_id() {
self.pending_nodes.push(NodeRenderState {
id: mask_id,
visited_children: false,
clip_bounds: None,
visited_mask: false,
mask: true,
});
}
self.pending_nodes.add_mask(element);
}
}
}
@@ -832,43 +1018,22 @@ impl RenderState {
self.focus_mode.exit(&element.id);
}
#[inline]
pub fn get_tile_bounds(&mut self, tile: tiles::Tile) -> Rect {
tiles::get_tile_bounds(self.viewbox, tile, self.get_scale())
}
#[inline]
pub fn get_current_tile_bounds(&mut self) -> Rect {
let tiles::Tile(tile_x, tile_y) = self.current_tile.unwrap();
let scale = self.get_scale();
let offset_x = self.viewbox.area.left * scale;
let offset_y = self.viewbox.area.top * scale;
Rect::from_xywh(
(tile_x as f32 * tiles::TILE_SIZE) - offset_x,
(tile_y as f32 * tiles::TILE_SIZE) - offset_y,
tiles::TILE_SIZE,
tiles::TILE_SIZE,
)
self.get_tile_bounds(self.current_tile.unwrap())
}
// Returns the bounds of the current tile relative to the viewbox,
// aligned to the nearest tile grid origin.
//
// Unlike `get_current_tile_bounds`, which calculates bounds using the exact
// scaled offset of the viewbox, this method snaps the origin to the nearest
// lower multiple of `TILE_SIZE`. This ensures the tile bounds are aligned
// with the global tile grid, which is useful for rendering tiles in a
/// consistent and predictable layout.
#[inline]
pub fn get_current_aligned_tile_bounds(&mut self) -> Rect {
let tiles::Tile(tile_x, tile_y) = self.current_tile.unwrap();
let scale = self.get_scale();
let start_tile_x =
(self.viewbox.area.left * scale / tiles::TILE_SIZE).floor() * tiles::TILE_SIZE;
let start_tile_y =
(self.viewbox.area.top * scale / tiles::TILE_SIZE).floor() * tiles::TILE_SIZE;
Rect::from_xywh(
(tile_x as f32 * tiles::TILE_SIZE) - start_tile_x,
(tile_y as f32 * tiles::TILE_SIZE) - start_tile_y,
tiles::TILE_SIZE,
tiles::TILE_SIZE,
)
tiles::get_tile_aligned_bounds(self.viewbox, self.current_tile.unwrap(), self.get_scale())
}
pub fn render_shape_tree_partial_uncached(
pub fn render_shape_tree_full(
&mut self,
tree: &ShapesPool,
modifiers: &HashMap<Uuid, Matrix>,
@@ -876,9 +1041,11 @@ impl RenderState {
scale_content: &HashMap<Uuid, f32>,
timestamp: i32,
) -> Result<(bool, bool), String> {
emscripten::log!(emscripten::Log::Default, "render_shape_tree_full:begin");
let mut iteration = 0;
let mut is_empty = true;
while let Some(node_render_state) = self.pending_nodes.pop() {
let scale = self.get_scale();
while let Some(node_render_state) = self.pending_nodes.next() {
let NodeRenderState {
id: node_id,
visited_children,
@@ -896,7 +1063,129 @@ impl RenderState {
// If the shape is not in the tile set, then we update
// it.
if self.tiles.get_tiles_of(node_id).is_none() {
self.update_tile_for(element);
self.update_tiles_for(element);
}
if visited_children {
self.render_shape_exit(
element,
visited_mask,
modifiers.get(&node_id),
scale_content.get(&element.id),
);
continue;
}
if !node_render_state.is_root() {
// No necesitamos esto porque en el primer render es imposible
// que se estén transformando elementos.
/*
let mut transformed_element: Cow<Shape> = Cow::Borrowed(element);
if let Some(modifier) = modifiers.get(&node_id) {
transformed_element.to_mut().apply_transform(modifier);
}
let is_visible = transformed_element.extrect().intersects(self.render_area)
&& !transformed_element.hidden
&& !transformed_element.visually_insignificant(scale);
if self.options.is_debug_visible() {
debug::render_debug_shape(self, &transformed_element, is_visible);
}
*/
let is_visible = !element.hidden
&& !element.visually_insignificant(scale);
if self.options.is_debug_visible() {
debug::render_debug_shape(self, element, is_visible);
}
if !is_visible {
continue;
}
}
self.render_shape_enter(element, mask);
if !node_render_state.is_root() && self.focus_mode.is_active() {
self.render_shape(
element,
modifiers.get(&element.id),
scale_content.get(&element.id),
);
} else if visited_children {
// NOTA: Esto no debería usarse porque esta función utiliza el CURRENT.
self.apply_drawing_to_full_render_canvas(Some(element));
}
// Set the node as visited_children before processing children
self.pending_nodes.add_children_safeguard(element, mask);
if element.is_recursive() {
let children_clip_bounds =
node_render_state.get_children_clip_bounds(element, modifiers.get(&element.id));
let mut children_ids =
element.modified_children_ids(structure.get(&element.id), false);
// Z-index ordering on Layouts
if element.has_layout() {
children_ids.sort_by(|id1, id2| {
let z1 = tree.get(id1).map_or_else(|| 0, |s| s.z_index());
let z2 = tree.get(id2).map_or_else(|| 0, |s| s.z_index());
z1.cmp(&z2)
});
}
for child_id in children_ids.iter() {
self.pending_nodes.add_child(child_id, children_clip_bounds);
}
}
// We try to avoid doing too many calls to get_time
if self.should_stop_rendering(iteration, timestamp) {
return Ok((is_empty, true));
}
iteration += 1;
}
emscripten::log!(emscripten::Log::Default, "render_shape_tree_full:end");
self.render_in_progress = false;
self.apply_full_render_to_final_canvas();
Ok((false, true))
}
pub fn render_shape_tree_partial_uncached(
&mut self,
tree: &ShapesPool,
modifiers: &HashMap<Uuid, Matrix>,
structure: &HashMap<Uuid, Vec<StructureEntry>>,
scale_content: &HashMap<Uuid, f32>,
timestamp: i32,
) -> Result<(bool, bool), String> {
let mut iteration = 0;
let mut is_empty = true;
let scale = self.get_scale();
while let Some(node_render_state) = self.pending_nodes.next() {
let NodeRenderState {
id: node_id,
visited_children,
clip_bounds: _,
visited_mask,
mask,
} = node_render_state;
is_empty = false;
let element = tree.get(&node_id).ok_or(
"Error: Element with root_id {node_render_state.id} not found in the tree."
.to_string(),
)?;
// If the shape is not in the tile set, then we update
// it.
if self.tiles.get_tiles_of(node_id).is_none() {
self.update_tiles_for(element);
}
if visited_children {
@@ -918,7 +1207,7 @@ impl RenderState {
let is_visible = transformed_element.extrect().intersects(self.render_area)
&& !transformed_element.hidden
&& !transformed_element.visually_insignificant(self.get_scale());
&& !transformed_element.visually_insignificant(scale);
if self.options.is_debug_visible() {
debug::render_debug_shape(self, &transformed_element, is_visible);
@@ -941,13 +1230,7 @@ impl RenderState {
}
// Set the node as visited_children before processing children
self.pending_nodes.push(NodeRenderState {
id: node_id,
visited_children: true,
clip_bounds: None,
visited_mask: false,
mask,
});
self.pending_nodes.add_children_safeguard(element, mask);
if element.is_recursive() {
let children_clip_bounds =
@@ -966,13 +1249,7 @@ impl RenderState {
}
for child_id in children_ids.iter() {
self.pending_nodes.push(NodeRenderState {
id: *child_id,
visited_children: false,
clip_bounds: children_clip_bounds,
visited_mask: false,
mask: false,
});
self.pending_nodes.add_child(child_id, children_clip_bounds);
}
}
@@ -993,10 +1270,12 @@ impl RenderState {
scale_content: &HashMap<Uuid, f32>,
timestamp: i32,
) -> Result<(), String> {
emscripten::log!(emscripten::Log::Default, "render_shape_tree_partial:begin");
let mut should_stop = false;
while !should_stop {
if let Some(current_tile) = self.current_tile {
if self.surfaces.has_cached_tile_surface(current_tile) {
emscripten::log!(emscripten::Log::Default, "cached");
performance::begin_measure!("render_shape_tree::cached");
let tile_rect = self.get_current_tile_bounds();
self.surfaces.draw_cached_tile_surface(
@@ -1015,6 +1294,7 @@ impl RenderState {
);
}
} else {
emscripten::log!(emscripten::Log::Default, "uncached");
performance::begin_measure!("render_shape_tree::uncached");
let (is_empty, early_return) = self.render_shape_tree_partial_uncached(
tree,
@@ -1027,16 +1307,7 @@ impl RenderState {
return Ok(());
}
performance::end_measure!("render_shape_tree::uncached");
let tile_rect = self.get_current_tile_bounds();
if !is_empty {
self.apply_render_to_final_canvas(tile_rect);
} else {
self.surfaces.apply_mut(SurfaceId::Target as u32, |s| {
let mut paint = skia::Paint::default();
paint.set_color(self.background_color);
s.canvas().draw_rect(tile_rect, &paint);
});
}
self.render_current_tile_to_final_canvas(is_empty);
}
}
@@ -1053,7 +1324,7 @@ impl RenderState {
// If we finish processing every node rendering is complete
// let's check if there are more pending nodes
if let Some(next_tile) = self.pending_tiles.pop() {
self.update_render_context(next_tile);
self.update_render_context(&next_tile);
if !self.surfaces.has_cached_tile_surface(next_tile) {
if let Some(ids) = self.tiles.get_shapes_at(next_tile) {
@@ -1062,19 +1333,9 @@ impl RenderState {
.iter()
.filter_map(|id| root_ids.get(id).map(|_| *id))
.collect();
// These shapes for the tile should be ordered as they are in the parent node
valid_ids.sort_by_key(|id| root_ids.get_index_of(id));
self.pending_nodes.extend(valid_ids.into_iter().map(|id| {
NodeRenderState {
id,
visited_children: false,
clip_bounds: None,
visited_mask: false,
mask: false,
}
}));
self.pending_nodes.extend(valid_ids);
}
}
} else {
@@ -1093,7 +1354,7 @@ impl RenderState {
ui::render(self, tree, modifiers, structure);
debug::render_wasm_label(self);
emscripten::log!(emscripten::Log::Default, "render_shape_tree_partial:end");
Ok(())
}
@@ -1102,7 +1363,7 @@ impl RenderState {
tiles::get_tiles_for_rect(shape.extrect(), tile_size)
}
pub fn update_tile_for(&mut self, shape: &Shape) {
pub fn update_tiles_for(&mut self, shape: &Shape) {
let TileRect(rsx, rsy, rex, rey) = self.get_tiles_for_shape(shape);
let new_tiles: HashSet<tiles::Tile> = (rsx..=rex)
.flat_map(|x| (rsy..=rey).map(move |y| tiles::Tile(x, y)))
@@ -1144,7 +1405,7 @@ impl RenderState {
if let Some(modifier) = modifiers.get(&shape_id) {
shape.to_mut().apply_transform(modifier);
}
self.update_tile_for(&shape);
self.update_tiles_for(&shape);
} else {
// We only need to rebuild tiles from the first level.
let children = shape.modified_children_ids(structure.get(&shape.id), false);
@@ -1174,7 +1435,7 @@ impl RenderState {
if let Some(modifier) = modifiers.get(&shape_id) {
shape.to_mut().apply_transform(modifier);
}
self.update_tile_for(&shape);
self.update_tiles_for(&shape);
}
let children = shape.modified_children_ids(structure.get(&shape.id), false);
@@ -1191,7 +1452,7 @@ impl RenderState {
if let Some(shape) = tree.get(uuid) {
let mut shape: Cow<Shape> = Cow::Borrowed(shape);
shape.to_mut().apply_transform(matrix);
self.update_tile_for(&shape);
self.update_tiles_for(&shape);
}
}
}

View File

@@ -1,12 +1,12 @@
use crate::options;
#[derive(Debug, Copy, Clone, PartialEq, Default)]
pub struct RenderOptions {
pub struct RenderStateOptions {
pub flags: u32,
pub dpr: Option<f32>,
}
impl RenderOptions {
impl RenderStateOptions {
pub fn is_debug_visible(&self) -> bool {
self.flags & options::DEBUG_VISIBLE == options::DEBUG_VISIBLE
}

View File

@@ -26,6 +26,7 @@ pub enum SurfaceId {
InnerShadows = 0b0_0100_0000,
UI = 0b0_1000_0000,
Debug = 0b1_0000_0000,
Full = 0b10_0000_0000,
}
pub struct Surfaces {
@@ -46,6 +47,8 @@ pub struct Surfaces {
ui: skia::Surface,
// for drawing debug info.
debug: skia::Surface,
// for slicing a full render.
full: skia::Surface,
// for drawing tiles.
tiles: TileTextureCache,
sampling_options: skia::SamplingOptions,
@@ -80,6 +83,7 @@ impl Surfaces {
let ui = gpu_state.create_surface_with_dimensions("ui".to_string(), width, height);
let debug = gpu_state.create_surface_with_dimensions("debug".to_string(), width, height);
let full = gpu_state.create_surface_with_dimensions("full".to_string(), width, height);
let tiles = TileTextureCache::new();
Surfaces {
@@ -92,6 +96,7 @@ impl Surfaces {
shape_strokes,
ui,
debug,
full,
tiles,
sampling_options,
margins,
@@ -172,6 +177,9 @@ impl Surfaces {
if ids & SurfaceId::Debug as u32 != 0 {
f(self.get_mut(SurfaceId::Debug));
}
if ids & SurfaceId::Full as u32 != 0 {
f(self.get_mut(SurfaceId::Full));
}
performance::begin_measure!("apply_mut::flags");
}
@@ -205,6 +213,7 @@ impl Surfaces {
SurfaceId::Strokes => &mut self.shape_strokes,
SurfaceId::Debug => &mut self.debug,
SurfaceId::UI => &mut self.ui,
SurfaceId::Full => &mut self.full,
}
}
@@ -213,6 +222,7 @@ impl Surfaces {
self.target = target;
self.debug = self.target.new_surface_with_dimensions(dim).unwrap();
self.ui = self.target.new_surface_with_dimensions(dim).unwrap();
self.full = self.target.new_surface_with_dimensions(dim).unwrap();
// The rest are tile size surfaces
}

View File

@@ -816,11 +816,15 @@ impl Shape {
}
}
pub fn is_root(&self) -> bool {
self.id.is_nil()
}
pub fn is_recursive(&self) -> bool {
matches!(
self.shape_type,
Type::Frame(_) | Type::Group(_) | Type::Bool(_)
)
) || self.id.is_nil()
}
pub fn add_shadow(&mut self, shadow: Shadow) {

View File

@@ -55,13 +55,14 @@ impl State {
.render_from_cache(&self.shapes, &self.modifiers, &self.structure);
}
pub fn start_render_loop(&mut self, timestamp: i32) -> Result<(), String> {
pub fn start_render_loop(&mut self, timestamp: i32, full: bool) -> Result<(), String> {
self.render_state.start_render_loop(
&self.shapes,
&self.modifiers,
&self.structure,
&self.scale_content,
timestamp,
full,
)?;
Ok(())
}
@@ -133,13 +134,13 @@ impl State {
// We don't need to update the tile for the root shape.
if !shape.id.is_nil() {
self.render_state.update_tile_for(&shape);
self.render_state.update_tiles_for(&shape);
}
}
pub fn update_tile_for_shape(&mut self, shape_id: Uuid) {
if let Some(shape) = self.shapes.get(&shape_id) {
self.render_state.update_tile_for(shape);
self.render_state.update_tiles_for(shape);
}
}
@@ -148,7 +149,7 @@ impl State {
panic!("Invalid current shape")
};
if !shape.id.is_nil() {
self.render_state.update_tile_for(&shape.clone());
self.render_state.update_tiles_for(&shape.clone());
}
}

View File

@@ -112,6 +112,38 @@ pub fn get_tile_rect(tile: Tile, scale: f32) -> skia::Rect {
skia::Rect::from_xywh(tx, ty, ts, ts)
}
pub fn get_tile_bounds(viewbox: Viewbox, Tile(tile_x, tile_y): Tile, scale: f32) -> skia::Rect {
let offset_x = viewbox.area.left * scale;
let offset_y = viewbox.area.top * scale;
skia::Rect::from_xywh(
(tile_x as f32 * TILE_SIZE) - offset_x,
(tile_y as f32 * TILE_SIZE) - offset_y,
TILE_SIZE,
TILE_SIZE,
)
}
// Returns the bounds of the current tile relative to the viewbox,
// aligned to the nearest tile grid origin.
//
// Unlike `get_current_tile_bounds`, which calculates bounds using the exact
// scaled offset of the viewbox, this method snaps the origin to the nearest
// lower multiple of `TILE_SIZE`. This ensures the tile bounds are aligned
// with the global tile grid, which is useful for rendering tiles in a
// consistent and predictable layout.
pub fn get_tile_aligned_bounds(viewbox: Viewbox, Tile(tile_x, tile_y): Tile, scale: f32) -> skia::Rect {
let start_tile_x =
(viewbox.area.left * scale / TILE_SIZE).floor() * TILE_SIZE;
let start_tile_y =
(viewbox.area.top * scale / TILE_SIZE).floor() * TILE_SIZE;
skia::Rect::from_xywh(
(tile_x as f32 * TILE_SIZE) - start_tile_x,
(tile_y as f32 * TILE_SIZE) - start_tile_y,
TILE_SIZE,
TILE_SIZE,
)
}
// This structure is usseful to keep all the shape uuids by shape id.
pub struct TileHashMap {
grid: HashMap<Tile, IndexSet<Uuid>>,
@@ -176,6 +208,10 @@ impl PendingTiles {
}
}
pub fn len(&self) -> usize {
self.list.len()
}
pub fn update(&mut self, tile_viewbox: &TileViewbox) {
self.list.clear();