Skip to content

Commit b167b80

Browse files
authored
feat(canvas): rasterize 2D on a render thread by default (#165)
* feat(canvas): rasterize 2D on a render thread by default 2D calls now record into pictures on the JS thread; a shared render thread replays them onto the real GPU context and presents. On a busy screen Skia's flush took ~6 ms of main-thread CPU per frame and starved every view animating beside the canvas. Opt out with getContext('2d', { threaded: false }) or Canvas.threaded2D = false. - Android and iOS: threaded creation, surface updates, readbacks routed to the render thread. - Android: 2D keeps its contents across swaps (offscreen surface copied to the window). - Video into a threaded canvas: HardwareBuffer (Android) and CVMetalTexture (iOS), zero-copy. - putImageData honours the dirty rect; reset() drops clips and saves. - Busy-screen benchmark: canvas-busy page, busy-bench.sh, busy-trace.sh. * feat(canvas): rasterize 2D on the render thread on Windows Windows now takes the threaded 2D path by default too (Canvas.threaded2D = true): the real context lives on the shared render thread's Direct3D 12 device, which presents; the JS thread only records. The render thread never waits on the UI thread. - Creation: canvas_native_context_create_d3d_threaded; the render thread keeps contexts boxed, so register_d3d() gets their final address. Falls back to unthreaded D3D, then CPU. - Swapchain panels: the swapchain is made on the render thread and bound on the UI thread. - XAML SurfaceImageSources (every alpha canvas): XamlHandoff. The render thread begins, draws and suspends through ISurfaceImageSourceNativeWithD2D and posts the UI thread's message-only window, which ends the draw (EndDraw must run on the UI thread). SetDevice stays there too. - Presents the display was not ready for are retried on the render thread. - Device loss: lost flags from the render thread; restore releases what still shows lost canvases from the UI thread, then shows the canvas again and resets the recorder's state. __simulateD3DDeviceRemoval and __d3dAdapterInfo cover the render thread's device. - drawImage(video): canvas-media's shared frame is opened on the rasterizing device, waited on (ready fence) and given back after Skia submits (release fence), zero-copy. - Tests: threaded_d3d (panel, resize, shared video frame) and threaded_d3d_lost. * chore: 3.0.0-alpha.22
1 parent 94ee40f commit b167b80

265 files changed

Lines changed: 189265 additions & 167355 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

‎apps/demo/src/main-page.xml‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
<StackLayout>
88
<Button text="canvas-spec" tap="{{ viewDemo }}" class="btn btn-primary view-demo"/>
99
<Button text="canvas-perf" tap="{{ viewDemo }}" class="btn btn-primary view-demo"/>
10+
<Button text="canvas-busy" tap="{{ viewDemo }}" class="btn btn-primary view-demo"/>
1011
<Button text="canvas" tap="{{ viewDemo }}" class="btn btn-primary view-demo"/>
1112
<Button text="canvas-babylon" tap="{{ viewDemo }}" class="btn btn-primary view-demo"/>
1213
<Button text="canvas-media" tap="{{ viewDemo }}" class="btn btn-primary view-demo"/>
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
import { EventData, GridLayout, Observable, Page } from '@nativescript/core';
2+
import { runBusyBench } from '@demo/shared';
3+
import { launchArgs } from '../launch-args';
4+
5+
// adb shell am start -n org.nativescript.plugindemo/com.tns.NativeScriptActivity --es demo canvas-busy --es suite 2d+views
6+
export function navigatingTo(args: EventData) {
7+
const page = <Page>args.object;
8+
page.bindingContext = new BusyModel();
9+
}
10+
11+
class BusyModel extends Observable {
12+
private ran = false;
13+
14+
constructor() {
15+
super();
16+
this.set('status', 'starting…');
17+
}
18+
19+
hostLoaded(args: EventData) {
20+
if (this.ran) {
21+
return;
22+
}
23+
this.ran = true;
24+
const scenario = launchArgs.suite ?? '2d+views';
25+
this.set('status', `running ${scenario}…`);
26+
runBusyBench(args.object as GridLayout, scenario)
27+
.then(() => this.set('status', `${scenario} done — see logcat (BUSY|…)`))
28+
.catch((e) => {
29+
console.log(`BUSY|${scenario}|error|${e?.message ?? e}`);
30+
this.set('status', `failed: ${e?.message ?? e}`);
31+
});
32+
}
33+
}
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
<Page xmlns="http://schemas.nativescript.org/tns.xsd" navigatingTo="navigatingTo" class="page">
2+
<Page.actionBar>
3+
<ActionBar title="canvas busy screen" icon="" class="action-bar" />
4+
</Page.actionBar>
5+
<GridLayout rows="*, auto">
6+
<GridLayout row="0" id="host" loaded="{{ hostLoaded }}" />
7+
<Label row="1" text="{{ status }}" textWrap="true" class="p-10" />
8+
</GridLayout>
9+
</Page>

‎crates/canvas-2d/Cargo.toml‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@ bitflags = "2.6.0"
2929
regex-lite = { workspace = true }
3030

3131
[target.'cfg(target_os = "windows")'.dependencies]
32-
windows = { workspace = true, optional = true, features = ["Win32_Graphics_Direct3D11", "Win32_Graphics_Direct3D11on12", "Win32_Graphics_Direct3D12", "Win32_Graphics_Dxgi_Common"] }
32+
windows = { workspace = true, optional = true, features = ["Win32_Foundation", "Win32_Graphics_Direct3D11", "Win32_Graphics_Direct3D11on12", "Win32_Graphics_Direct3D12", "Win32_Graphics_Dxgi_Common"] }
3333

3434
[target.'cfg(any(target_os = "ios", target_os="macos", target_os = "visionos", target_os = "tvos"))'.dependencies]
3535
foreign-types-shared = "0.3.1"

‎crates/canvas-2d/src/context/drawing_images/mod.rs‎

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,7 @@ impl Context {
4848
)
4949
} else if let Some((dimensions, bytes)) = bytes {
5050
let (width, height) = dimensions;
51-
if let Some(image) = crate::utils::image::from_image_slice_no_copy(
51+
if let Some(image) = self.asset_image(
5252
bytes,
5353
width as i32,
5454
height as i32,
@@ -83,7 +83,7 @@ impl Context {
8383
self.draw_image(image, src_rect, dst_rect)
8484
} else if let Some((dimensions, bytes)) = bytes {
8585
let (width, height) = dimensions;
86-
if let Some(image) = crate::utils::image::from_image_slice_no_copy(
86+
if let Some(image) = self.asset_image(
8787
bytes,
8888
width as i32,
8989
height as i32,
@@ -107,7 +107,7 @@ impl Context {
107107
self.draw_image_dx_dy(image, x, y)
108108
} else if let Some((dimensions, bytes)) = bytes {
109109
let (width, height) = dimensions;
110-
if let Some(image) = crate::utils::image::from_image_slice_no_copy(
110+
if let Some(image) = self.asset_image(
111111
bytes,
112112
width as i32,
113113
height as i32,
@@ -139,7 +139,7 @@ impl Context {
139139
} else if let Some((dimensions, bytes)) = bytes {
140140
let (w, h) = dimensions;
141141
if let Some(image) =
142-
crate::utils::image::from_image_slice_no_copy(bytes, w as i32, h as i32)
142+
self.asset_image(bytes, w as i32, h as i32)
143143
{
144144
self.draw_image_dx_dy_dw_dh(&image, x, y, width, height)
145145
}
@@ -162,7 +162,7 @@ impl Context {
162162
self.draw_image_with_rect(image, dst_rect)
163163
} else if let Some((dimensions, bytes)) = bytes {
164164
let (width, height) = dimensions;
165-
if let Some(image) = crate::utils::image::from_image_slice_no_copy(
165+
if let Some(image) = self.asset_image(
166166
bytes,
167167
width as i32,
168168
height as i32,

‎crates/canvas-2d/src/context/drawing_paths/mod.rs‎

Lines changed: 20 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -155,24 +155,25 @@ impl Context {
155155

156156
pub fn clip(&mut self, path: Option<&mut Path>, fill_rule: Option<FillRule>) {
157157
let fill_rule = fill_rule.unwrap_or(FillRule::NonZero);
158-
match path {
159-
Some(path) => {
160-
let current = path.fill_type();
161-
path.set_fill_type(fill_rule);
162-
self.surface
163-
.canvas()
164-
.clip_path(path.path(), Some(ClipOp::Intersect), Some(true));
165-
path.set_fill_type_raw(current);
166-
}
167-
None => {
168-
let current = self.path.fill_type();
169-
self.path.set_fill_type(fill_rule);
170-
self.surface
171-
.canvas()
172-
.clip_path(self.path.path(), Some(ClipOp::Intersect), Some(true));
173-
self.path.set_fill_type_raw(current);
174-
}
175-
}
158+
let path = match path {
159+
Some(path) => path,
160+
None => &mut self.path,
161+
};
162+
let current = path.fill_type();
163+
path.set_fill_type(fill_rule);
164+
let clip = path.path().clone();
165+
path.set_fill_type_raw(current);
166+
167+
let canvas = match self.recording.as_mut() {
168+
Some(recording) => recording.canvas(),
169+
None => self.surface.canvas(),
170+
};
171+
let matrix = canvas.local_to_device_as_3x3();
172+
self.state.clips.push(crate::context::recording::DeviceClip {
173+
path: clip.with_transform(&matrix),
174+
anti_alias: true,
175+
});
176+
canvas.clip_path(&clip, Some(ClipOp::Intersect), Some(true));
176177
}
177178

178179
/// The point is device-space per spec, paths are user-space. Note `state.matrix`
@@ -181,7 +182,7 @@ impl Context {
181182
if !x.is_finite() || !y.is_finite() {
182183
return None;
183184
}
184-
let matrix = self.surface.canvas().local_to_device_as_3x3();
185+
let matrix = self.canvas().local_to_device_as_3x3();
185186
if !is_invertible(&matrix) {
186187
return None;
187188
}

‎crates/canvas-2d/src/context/drawing_rectangles.rs‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ fn clear_paint() -> &'static Paint {
2020
impl Context {
2121
#[inline]
2222
pub fn clear_rect(&mut self, x: c_float, y: c_float, width: c_float, height: c_float) {
23+
self.discard_if_cleared(&Rect::from_xywh(x, y, width, height));
2324
let paint = clear_paint();
2425
self.render_to_canvas(paint, |canvas, paint| {
2526
let rect = Rect::from_xywh(x, y, width, height);

‎crates/canvas-2d/src/context/mod.rs‎

Lines changed: 54 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -6,9 +6,8 @@ use base64::Engine;
66
use skia_safe::image::CachingHint;
77
use skia_safe::BlendMode;
88
pub use skia_safe::ColorType;
9-
use skia_safe::{
10-
AlphaType, Color, EncodedImageFormat, IPoint, ISize, Image, ImageInfo, Point, Surface,
11-
};
9+
pub use skia_safe::Image;
10+
use skia_safe::{AlphaType, Color, EncodedImageFormat, IPoint, ISize, ImageInfo, Point, Surface};
1211

1312
use compositing::composite_operation_type::CompositeOperationType;
1413
use fill_and_stroke_styles::paint::Paint;
@@ -32,6 +31,7 @@ pub mod drawing_images;
3231
pub mod drawing_text;
3332
pub mod fill_and_stroke_styles;
3433
pub mod paths;
34+
pub mod recording;
3535
pub mod pixel_manipulation;
3636
pub mod text_styles;
3737

@@ -91,6 +91,9 @@ pub struct State {
9191
pub(crate) letter_spacing: f32,
9292
pub(crate) matrix: skia_safe::Matrix,
9393
pub(crate) clip: Option<Path>,
94+
/// Device space; re-applied at picture boundaries.
95+
pub(crate) clips: Vec<recording::DeviceClip>,
96+
pub(crate) saved_matrix: skia_safe::M44,
9497
}
9598

9699
impl Default for State {
@@ -127,6 +130,8 @@ impl Default for State {
127130
letter_spacing: 0.,
128131
matrix: skia_safe::Matrix::new_identity(),
129132
clip: None,
133+
clips: Vec::new(),
134+
saved_matrix: skia_safe::M44::new_identity(),
130135
}
131136
}
132137
}
@@ -213,6 +218,10 @@ pub struct Context {
213218
// native context first leaves GrDirectContext tearing itself down against
214219
// an already-destroyed device.
215220
pub(crate) surface: Surface,
221+
/// Android: `surface` is then offscreen, as a swap leaves the window's next buffer undefined.
222+
/// Must drop before `direct_context`.
223+
#[cfg(feature = "gl")]
224+
pub(crate) window_surface: Option<Surface>,
216225
pub(crate) surface_state: SurfaceState,
217226
#[cfg(any(feature = "gl", feature = "vulkan", feature = "metal", feature = "d3d"))]
218227
pub(crate) direct_context: Option<skia_safe::gpu::DirectContext>,
@@ -234,6 +243,7 @@ pub struct Context {
234243
pub(crate) state: State,
235244
pub(crate) state_stack: Vec<State>,
236245
pub(crate) font_color: Color,
246+
pub(crate) recording: Option<recording::Recording>,
237247
}
238248

239249
impl Drop for Context {
@@ -376,6 +386,13 @@ impl Context {
376386
/// Runs before every draw. Skia uploads a raster image the moment a draw records it (a pattern
377387
/// fill, say), so another canvas's GL context being current puts the texture in the wrong one.
378388
pub(crate) fn ensure_current(&mut self) {
389+
if self.recording.is_none() {
390+
self.bind_surface();
391+
}
392+
}
393+
394+
#[inline]
395+
pub(crate) fn bind_surface(&mut self) {
379396
#[cfg(feature = "gl")]
380397
if let Some(ref context) = self.gl_context {
381398
context.make_current();
@@ -386,13 +403,21 @@ impl Context {
386403
}
387404
}
388405

406+
#[inline]
407+
pub(crate) fn canvas(&mut self) -> &skia_safe::Canvas {
408+
match self.recording.as_mut() {
409+
Some(recording) => recording.canvas(),
410+
None => self.surface.canvas(),
411+
}
412+
}
413+
389414
#[inline]
390415
pub fn with_canvas<F>(&mut self, f: F)
391416
where
392417
F: FnOnce(&skia_safe::Canvas),
393418
{
394419
self.ensure_current();
395-
f(self.surface.canvas());
420+
f(self.canvas());
396421
}
397422

398423
#[inline]
@@ -401,7 +426,11 @@ impl Context {
401426
F: FnOnce(&skia_safe::Canvas, &mut Path),
402427
{
403428
self.ensure_current();
404-
f(self.surface.canvas(), &mut self.path);
429+
let canvas = match self.recording.as_mut() {
430+
Some(recording) => recording.canvas(),
431+
None => self.surface.canvas(),
432+
};
433+
f(canvas, &mut self.path);
405434
self.surface_state = self.surface_state | SurfaceState::Pending;
406435
}
407436

@@ -411,7 +440,7 @@ impl Context {
411440
F: FnOnce(&skia_safe::Canvas),
412441
{
413442
self.ensure_current();
414-
f(self.surface.canvas());
443+
f(self.canvas());
415444
self.surface_state = self.surface_state | SurfaceState::Pending;
416445
}
417446

@@ -421,7 +450,11 @@ impl Context {
421450
F: FnOnce(&skia_safe::Canvas, &Paint),
422451
{
423452
self.ensure_current();
424-
f(self.surface.canvas(), &self.state.paint);
453+
let canvas = match self.recording.as_mut() {
454+
Some(recording) => recording.canvas(),
455+
None => self.surface.canvas(),
456+
};
457+
f(canvas, &self.state.paint);
425458
self.surface_state = self.surface_state | SurfaceState::Pending;
426459
}
427460

@@ -438,7 +471,7 @@ impl Context {
438471
F: FnOnce(&skia_safe::Canvas),
439472
{
440473
self.ensure_current();
441-
f(self.surface.canvas());
474+
f(self.canvas());
442475
self.surface_state = self.surface_state | SurfaceState::Pending;
443476
}
444477

@@ -483,7 +516,7 @@ impl Context {
483516
}
484517

485518
pub fn draw_pixels(&mut self, image: &Image) {
486-
let canvas = self.surface.canvas();
519+
let canvas = self.canvas();
487520
canvas.save();
488521
canvas.reset_matrix();
489522
let mut paint = skia_safe::Paint::default();
@@ -634,7 +667,7 @@ impl Context {
634667
layer_paint.set_blend_mode(BlendMode::SrcOver);
635668
let mut layer_recorder = skia_safe::PictureRecorder::new();
636669
layer_recorder.begin_recording(self.surface_data.bounds, false);
637-
let current_matrix = self.surface.canvas().local_to_device();
670+
let current_matrix = self.canvas().local_to_device();
638671
if let Some(layer) = layer_recorder.recording_canvas() {
639672
layer.set_matrix(&current_matrix);
640673
f(layer, &layer_paint, &mut self.path);
@@ -643,7 +676,7 @@ impl Context {
643676
if let Some(pict) =
644677
layer_recorder.finish_recording_as_picture(Some(&self.surface_data.bounds))
645678
{
646-
let canvas = self.surface.canvas();
679+
let canvas = self.canvas();
647680
canvas.save();
648681
let mut blend_paint = skia_safe::Paint::default();
649682
blend_paint.set_anti_alias(true);
@@ -692,7 +725,7 @@ impl Context {
692725
layer_paint.set_blend_mode(BlendMode::SrcOver);
693726
let mut layer_recorder = skia_safe::PictureRecorder::new();
694727
layer_recorder.begin_recording(self.surface_data.bounds, false);
695-
let current_matrix = self.surface.canvas().local_to_device();
728+
let current_matrix = self.canvas().local_to_device();
696729
if let Some(layer) = layer_recorder.recording_canvas() {
697730
layer.set_matrix(&current_matrix);
698731
f(layer, &layer_paint);
@@ -701,7 +734,7 @@ impl Context {
701734
if let Some(pict) =
702735
layer_recorder.finish_recording_as_picture(Some(&self.surface_data.bounds))
703736
{
704-
let canvas = self.surface.canvas();
737+
let canvas = self.canvas();
705738
canvas.save();
706739
let mut blend_paint = skia_safe::Paint::default();
707740
blend_paint.set_anti_alias(true);
@@ -732,7 +765,7 @@ impl Context {
732765
| BlendMode::DstATop
733766
| BlendMode::Src
734767
) {
735-
f(self.surface.canvas(), paint);
768+
f(self.canvas(), paint);
736769
self.surface_state = self.surface_state | SurfaceState::Pending;
737770
return;
738771
}
@@ -758,7 +791,7 @@ impl Context {
758791
if let Some(pict) =
759792
layer_recorder.finish_recording_as_picture(Some(&self.surface_data.bounds))
760793
{
761-
let canvas = self.surface.canvas();
794+
let canvas = self.canvas();
762795
canvas.save();
763796
let mut blend_paint = skia_safe::Paint::default();
764797
blend_paint.set_anti_alias(true);
@@ -786,6 +819,12 @@ impl Context {
786819
self.state = State::default();
787820
self.state_stack.clear();
788821
self.state.direction = direction;
822+
let base = if self.recording.is_some() {
823+
recording::RECORDING_BASE
824+
} else {
825+
1
826+
};
827+
self.canvas().restore_to_count(base);
789828
}
790829

791830
pub fn clear_canvas(&mut self) {

0 commit comments

Comments
 (0)