Skip to content

Commit c716c69

Browse files
author
Roy Lin
committed
feat: integrate graphics GPU renderer boundary
1 parent 50f5f74 commit c716c69

8 files changed

Lines changed: 835 additions & 48 deletions

File tree

Cargo.lock

Lines changed: 651 additions & 25 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -92,6 +92,7 @@ default = ["headless", "authoring", "design-system", "software-reference"]
9292
headless = []
9393
graphics = ["dep:a3s-graphics"]
9494
software-reference = ["graphics", "a3s-graphics/software"]
95+
gpu = ["graphics", "a3s-graphics/gpu"]
9596
authoring = [
9697
"dep:swc_atoms",
9798
"dep:swc_common",
@@ -119,7 +120,7 @@ gtk4 = []
119120
gtk4-native = ["gtk4", "dep:gtk4_crate"]
120121

121122
[dependencies]
122-
a3s-graphics = { git = "https://github.com/A3S-Lab/Graphics", rev = "2cad948189eec816d38f0df01ea38993f92118a5", optional = true, default-features = false }
123+
a3s-graphics = { git = "https://github.com/A3S-Lab/Graphics", rev = "8748fab595f8dd7f7ca28767f1c58bd7f3f34ee0", optional = true, default-features = false }
123124
fixed_decimal = { version = "0.7.2", features = ["ryu"] }
124125
icu_collator = "2.2.1"
125126
icu_datetime = "2.2.0"
@@ -136,6 +137,9 @@ swc_ecma_ast = { version = "25.0.0", optional = true }
136137
swc_ecma_parser = { version = "41.1.2", optional = true }
137138
writeable = "0.6.3"
138139

140+
[dev-dependencies]
141+
pollster = "1.0.1"
142+
139143
[target.'cfg(target_os = "macos")'.dependencies]
140144
objc2 = { version = "0.6.4", optional = true }
141145
objc2-app-kit = { version = "0.3.2", default-features = false, features = [

README.md

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,8 +17,7 @@ describe semantic UI in Rust function components or `.rsx` modules, update
1717
state through reducers, and lower the same portable native IR into layout,
1818
paint, interaction, and accessibility records. Application pixels are moving
1919
to the shared [`a3s-graphics`](https://github.com/A3S-Lab/Graphics) engine,
20-
whose production GPU path is being built on `wgpu` directly for Metal,
21-
Direct3D 12, and Vulkan.
20+
whose GPU path uses `wgpu` directly for Metal, Direct3D 12, and Vulkan.
2221

2322
The current AppKit, GTK4, WinUI, and deterministic headless hosts remain as the
2423
migration baseline. New rendering work targets the self-drawn Graphics path;
@@ -130,6 +129,7 @@ just playground
130129
| `headless` | Deterministic runtime and host behavior without an OS GUI |
131130
| `graphics` | Pinned A3S Graphics scene vocabulary without a renderer backend |
132131
| `software-reference` | Deterministic retained reference renderer; implies `graphics` |
132+
| `gpu` | Owned offscreen GPU renderer and readback path; implies `graphics` |
133133
| `authoring` | SWC-backed RSX parsing, `ComponentCx`, and explicit component registries |
134134
| `design-system` | Built-in `rsx_ui` registry; implies `authoring` |
135135
| `appkit`, `gtk4`, `winui` | Legacy planning adapters retained for migration evidence |
@@ -144,6 +144,7 @@ cargo check --locked --no-default-features --lib
144144
cargo check --locked --no-default-features --features authoring --lib
145145
cargo check --locked --no-default-features --features graphics --lib
146146
cargo check --locked --no-default-features --features software-reference --lib
147+
cargo check --locked --no-default-features --features gpu --lib
147148
```
148149

149150
## Runtime Architecture

docs/architecture.md

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -147,9 +147,10 @@ backend boundary inside the current crate. `authoring` enables SWC-backed RSX
147147
parsing and `ComponentCx`; `design-system` depends on `authoring` and enables
148148
the built-in `rsx_ui` registry. `graphics` enables only the pinned engine scene
149149
contract, while `software-reference` adds deterministic retained
150-
rasterization. The GPU feature is introduced only when the M2 backend is real;
151-
there is no placeholder backend. The default feature set keeps the existing
152-
authoring experience plus software reference evidence, while
150+
rasterization. `gpu` adds the real offscreen renderer, capability report, and
151+
readback path from pinned Graphics commit `8748fab`; there is no placeholder
152+
backend. The default feature set keeps the existing authoring experience plus
153+
software reference evidence, while
153154
`cargo check --no-default-features --lib` proves that protocol, semantic,
154155
interaction, focus, selection, i18n, and accessibility remain usable without
155156
Graphics, SWC, or `rsx_ui`.

docs/roadmap.md

Lines changed: 15 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -169,9 +169,9 @@ The repository already provides:
169169

170170
The independent Graphics repository has a versioned scene, stable draw IDs,
171171
canonical fingerprints, retained damage, affine transforms, clipping, opacity,
172-
solid and rounded rectangles, borders, and a deterministic software renderer as
173-
of commit `2cad948`. Its GPU backend is not yet complete and must not be claimed
174-
as production-ready before M2 passes.
172+
solid and rounded rectangles, borders, a deterministic software renderer, and
173+
an owned `wgpu` rectangle backend as of commit `8748fab`. The GPU backend has
174+
local Direct3D 12 evidence; Metal and Vulkan CI evidence remains an M2 gate.
175175

176176
## Cleanup Inventory
177177

@@ -226,16 +226,18 @@ Landed evidence:
226226
- architecture and public docs now select A3S Graphics with no framework-owned
227227
renderer residue
228228
- the engine dependency is pinned to full commit
229-
`2cad948189eec816d38f0df01ea38993f92118a5`
230-
- `graphics` and `software-reference` separate scene consumers from reference
231-
rasterization while no-default remains semantic-only
229+
`8748fab595f8dd7f7ca28767f1c58bd7f3f34ee0`
230+
- `graphics`, `software-reference`, and `gpu` separate scene consumers from
231+
reference and accelerated rendering while no-default remains semantic-only
232232
- `ReferenceRenderer` preserves frame fingerprints and retained damage behind a
233233
GUI-owned error boundary
234+
- `GpuSceneRenderer` preserves the same scene/planner boundary and maps GPU and
235+
readback failures into the GUI error contract
234236
- the first compatibility cleanup removed class-name widget mapping shims and
235237
replaced broad dead-code allowances with target-accurate compilation
236238

237-
Remaining work is the field inventory, dependency-direction automation, and
238-
the first `NativeElement` layout-to-scene adapter.
239+
Remaining work is the field inventory and the first `NativeElement`
240+
layout-to-scene adapter.
239241

240242
Deliverables:
241243

@@ -259,24 +261,25 @@ Acceptance gates:
259261

260262
### M2 - Graphics GPU backend
261263

262-
Status: planned after M1.
264+
Status: implementation landed at Graphics commit `8748fab`; cross-platform CI
265+
evidence pending.
263266

264267
Deliverables:
265268

266269
- owned `wgpu` device selection and capability report
267270
- surface-independent sRGB render target and asynchronous readback
268271
- WGSL pipelines for fills, rounded rectangles, borders, affine transforms,
269272
clipping, opacity, and ordered source-over blending
270-
- bounded instance/staging buffers, pipeline cache, and frame diagnostics
271-
- typed adapter absence, device loss, out-of-memory, and surface errors
273+
- bounded grow-only instance buffers and frame diagnostics
274+
- typed adapter absence, device loss, validation, internal, out-of-memory,
275+
capacity, and readback errors
272276

273277
Acceptance gates:
274278

275279
- shader and pipeline validation passes for Metal, DX12, and Vulkan CI targets
276280
- GPU output matches the software fixtures within reviewed edge-AA thresholds
277281
- transparent overlap preserves command order
278282
- GPU-disabled Graphics builds contain no `wgpu` dependency
279-
- device recreation can replay all resources required by the rectangle fixture
280283

281284
### M3 - Generic layout and scene vertical slice
282285

justfile

Lines changed: 21 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -298,12 +298,30 @@ test-examples:
298298

299299
# Run adapter planning tests without native OS bindings
300300
test-platforms:
301-
cargo test --locked --features appkit,winui,gtk4
301+
cargo test --locked --features appkit,winui,gtk4,gpu
302+
303+
# Exercise both GUI-to-Graphics renderer boundaries
304+
test-graphics:
305+
cargo test --locked --no-default-features --features software-reference,gpu --lib drawing::
302306

303307
# Prove the runtime core builds without SWC or the built-in design system
304308
check-core:
305309
cargo check --locked --no-default-features --lib
306310
cargo check --locked --no-default-features --features authoring --lib
311+
cargo check --locked --no-default-features --features graphics --lib
312+
cargo check --locked --no-default-features --features software-reference --lib
313+
cargo check --locked --no-default-features --features gpu --lib
314+
315+
# Prove semantic-only builds do not acquire Graphics or wgpu
316+
check-core-graph:
317+
#!/usr/bin/env bash
318+
set -euo pipefail
319+
320+
core_graph="$(cargo tree --locked --no-default-features --prefix none)"
321+
if grep -Eq '^(a3s-graphics|wgpu) ' <<<"$core_graph"; then
322+
echo "graphics dependencies entered the semantic-only graph" >&2
323+
exit 1
324+
fi
307325
308326
# Run native-feature library tests for this operating system
309327
test-native:
@@ -343,14 +361,14 @@ winui-input-smoke EVIDENCE:
343361

344362
# Lint every target and deny high-confidence Clippy and Rust warnings
345363
clippy:
346-
cargo clippy --locked --all-targets --features appkit,winui,gtk4 -- -A clippy::all -D clippy::correctness -D clippy::suspicious -A clippy::unnecessary_get_then_check -D unused
364+
cargo clippy --locked --all-targets --features appkit,winui,gtk4,gpu -- -A clippy::all -D clippy::correctness -D clippy::suspicious -A clippy::unnecessary_get_then_check -D unused
347365

348366
# Build crate documentation and fail on rustdoc warnings
349367
doc-check:
350368
RUSTDOCFLAGS="-D warnings" cargo doc --locked --no-deps --document-private-items
351369

352370
# Run the full local verification suite
353-
verify: fmt-check check-core clippy doc-check test test-examples test-platforms diff-check
371+
verify: fmt-check check-core check-core-graph clippy doc-check test test-examples test-platforms test-graphics diff-check
354372

355373
# Run dogfood reducer and protocol-boundary regression tests
356374
dogfood-regression:

src/drawing.rs

Lines changed: 134 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,10 +10,18 @@ pub use a3s_graphics::{
1010
Primitive, Rect, Scene, SceneBuilder, Size, StrokeRect, SCENE_SCHEMA_VERSION,
1111
};
1212

13-
#[cfg(feature = "software-reference")]
14-
use a3s_graphics::{FramePlanner, SoftwareRenderer};
13+
#[cfg(feature = "gpu")]
14+
pub use a3s_graphics::{
15+
GpuBackend, GpuBackendPreference, GpuCapabilities, GpuDeviceType, GpuFrame, GpuPowerPreference,
16+
GpuReadback, GpuRendererOptions,
17+
};
1518

19+
#[cfg(any(feature = "software-reference", feature = "gpu"))]
20+
use a3s_graphics::FramePlanner;
1621
#[cfg(feature = "software-reference")]
22+
use a3s_graphics::SoftwareRenderer;
23+
24+
#[cfg(any(feature = "software-reference", feature = "gpu"))]
1725
use crate::GuiResult;
1826

1927
/// Metadata and pixels produced by one deterministic reference frame.
@@ -95,6 +103,69 @@ impl ReferenceRenderer {
95103
}
96104
}
97105

106+
/// GPU renderer that plans retained frames directly from GUI-produced scenes.
107+
#[cfg(feature = "gpu")]
108+
#[derive(Debug)]
109+
pub struct GpuSceneRenderer {
110+
planner: FramePlanner,
111+
renderer: a3s_graphics::GpuRenderer,
112+
}
113+
114+
#[cfg(feature = "gpu")]
115+
impl GpuSceneRenderer {
116+
pub async fn request(options: GpuRendererOptions) -> GuiResult<Self> {
117+
Ok(Self {
118+
planner: FramePlanner::new(),
119+
renderer: a3s_graphics::GpuRenderer::request(options).await?,
120+
})
121+
}
122+
123+
pub fn capabilities(&self) -> &GpuCapabilities {
124+
self.renderer.capabilities()
125+
}
126+
127+
pub async fn render(&mut self, scene: Scene) -> GuiResult<GpuFrame> {
128+
let frame = self.planner.plan(scene)?;
129+
Ok(self.renderer.render(&frame).await?)
130+
}
131+
132+
pub async fn request_readback(&self) -> GuiResult<GpuReadbackTicket> {
133+
Ok(GpuReadbackTicket {
134+
inner: self.renderer.request_readback().await?,
135+
})
136+
}
137+
138+
pub fn poll(&self) -> GuiResult<bool> {
139+
Ok(self.renderer.poll()?)
140+
}
141+
142+
pub fn wait(&self) -> GuiResult<()> {
143+
Ok(self.renderer.wait()?)
144+
}
145+
146+
pub fn reset(&mut self) {
147+
self.planner.reset();
148+
}
149+
}
150+
151+
/// GUI-error-aware handle for an asynchronous GPU readback.
152+
#[cfg(feature = "gpu")]
153+
#[derive(Debug)]
154+
pub struct GpuReadbackTicket {
155+
inner: a3s_graphics::GpuReadbackTicket,
156+
}
157+
158+
#[cfg(feature = "gpu")]
159+
impl GpuReadbackTicket {
160+
pub fn try_finish(&mut self) -> GuiResult<Option<GpuReadback>> {
161+
Ok(self.inner.try_finish()?)
162+
}
163+
164+
pub fn finish(self) -> GuiResult<GpuReadback> {
165+
Ok(self.inner.finish()?)
166+
}
167+
}
168+
98169
#[cfg(all(test, feature = "software-reference"))]
99170
mod tests {
100171
use super::*;
@@ -157,3 +228,64 @@ mod tests {
157228
assert_send_sync::<ReferenceRenderer>();
158229
}
159230
}
231+
232+
#[cfg(all(test, feature = "gpu"))]
233+
mod gpu_tests {
234+
use super::*;
235+
use crate::GuiError;
236+
237+
fn scene() -> Scene {
238+
let mut builder = SceneBuilder::new(Size::new(4.0, 3.0), 1.0, Color::WHITE);
239+
builder
240+
.push(DrawCommand::new(
241+
DrawId::new(1).unwrap(),
242+
Primitive::FillRect(FillRect {
243+
rect: Rect::new(1.0, 1.0, 2.0, 1.0),
244+
color: Color::BLACK,
245+
}),
246+
))
247+
.unwrap();
248+
builder.finish().unwrap()
249+
}
250+
251+
#[test]
252+
fn invalid_gpu_options_keep_the_gui_error_boundary() {
253+
let options = GpuRendererOptions {
254+
max_instances_per_frame: 0,
255+
..Default::default()
256+
};
257+
let error = pollster::block_on(GpuSceneRenderer::request(options)).unwrap_err();
258+
assert!(matches!(error, GuiError::Graphics { .. }));
259+
assert!(error.to_string().contains("max_instances_per_frame"));
260+
}
261+
262+
#[test]
263+
fn gpu_scene_renderer_completes_the_gui_scene_boundary() {
264+
let options = GpuRendererOptions {
265+
power_preference: GpuPowerPreference::None,
266+
allow_software_adapter: true,
267+
..Default::default()
268+
};
269+
let mut renderer = match pollster::block_on(GpuSceneRenderer::request(options)) {
270+
Ok(renderer) => renderer,
271+
Err(error) if error.to_string().contains("no compatible GPU adapter") => return,
272+
Err(error) => panic!("failed to initialize GUI test GPU: {error}"),
273+
};
274+
275+
let frame = pollster::block_on(renderer.render(scene())).unwrap();
276+
assert!(frame.rendered);
277+
let image = pollster::block_on(renderer.request_readback())
278+
.unwrap()
279+
.finish()
280+
.unwrap();
281+
assert_eq!(image.pixel(1, 1), Some(Color::BLACK));
282+
}
283+
284+
#[test]
285+
fn gpu_scene_renderer_is_send_and_sync() {
286+
fn assert_send_sync<T: Send + Sync>() {}
287+
fn assert_send<T: Send>() {}
288+
assert_send_sync::<GpuSceneRenderer>();
289+
assert_send::<GpuReadbackTicket>();
290+
}
291+
}

src/lib.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -103,6 +103,8 @@ pub use compiler::{
103103
CompiledBinding, CompiledBindingSource, CompiledProps, CompiledRsxNode, ComponentClassVariants,
104104
RsxCompilerBridge,
105105
};
106+
#[cfg(feature = "gpu")]
107+
pub use drawing::{GpuReadbackTicket, GpuSceneRenderer};
106108
#[cfg(feature = "software-reference")]
107109
pub use drawing::{ReferenceFrame, ReferenceRenderer};
108110
pub use effect::{

0 commit comments

Comments
 (0)