10 / 12 · Integrate, measure, ship集成、测量、交付
Integrate one pass into two hosts—and your engine让同一个 pass 接入两个宿主和自研引擎
Define a reusable rendering boundary, then account for depth, color and device ownership in an existing engine.定义可复用渲染边界,并在现有引擎里处理深度、色彩与设备所有权。
Cross-platform rendering starts with ownership and contracts. A shared source file is useful only if its host assumptions are explicit. The most reusable object in this project is the render pass, not the browser page.跨平台渲染从职责与约定开始。共享代码的前提是把宿主假设说清楚。本项目最适合复用的是渲染 pass,而不是浏览器页面。
1. Keep the platform at the edges1. 让平台差异留在边界
Browser code handles fetch, DOM events and requestAnimationFrame. Native code handles files, winit events and a monotonic clock. Both feed a Frame into WorkshopSurface, which calls the same WorkshopPass and WGSL. The model parser, time evaluation, ordering and data contract do not need a second implementation for these two hosts.浏览器代码负责 fetch、DOM 事件和 requestAnimationFrame;桌面代码负责文件、winit 事件与单调时钟。两者都把 Frame 交给 WorkshopSurface,后者调用同一个 WorkshopPass 和 WGSL。对于这两个宿主,模型解析、时间求值、排序和数据约定不需要另写一份。
wgpu translates the graphics work to supported backends. This sample selects D3D12 on Windows; macOS/Metal and Linux/Vulkan remain platform validation tasks until run there. A CUDA rasterizer would be a separate backend implementation sharing representation and algorithm contracts. CUDA is not a wgpu backend in this project.wgpu 将图形工作映射到支持的后端。此示例在 Windows 选择 D3D12;macOS/Metal 和 Linux/Vulkan 仍需在对应平台实测。CUDA 光栅器需要单独实现后端,共享的是表示和算法约定。本项目的 wgpu 后端里没有 CUDA。
2. Let the engine own the device and frame2. 让引擎拥有设备和帧
Inside an existing Rust/wgpu engine, use StgPass directly. Construct it with the engine’s Device and attachment format. Each frame, call prepare with the engine’s Queue, view matrix, projection matrix, viewport and normalized time. Open the engine’s render pass, call draw, then let the engine submit and present. Do not create another device inside this plugin.在已有 Rust/wgpu 引擎里直接使用 StgPass。用引擎的 Device 与附件格式构造它。每帧用引擎的 Queue、观察矩阵、投影矩阵、视口和归一化时间调用 prepare;开启引擎自己的 render pass,调用 draw,最后由引擎提交和显示。不要在插件里额外创建一台 device。
Submit one prepared frame before preparing another with the same mutable buffers. If a renderer prepares several cameras before submitting, each view needs its own buffer region or another explicit lifetime strategy. Otherwise the last write may supply data for earlier recorded work.同一组可变 buffer 准备完一帧后,应先提交,再准备下一帧。如果渲染器在提交前准备多个相机,每个视图需要独立 buffer 区域或明确的生命周期策略,否则最后一次写入可能被之前记录的工作使用。
3. Agree on depth and color3. 约定深度与色彩
Render opaque geometry first. Give the splat pipeline a compatible depth attachment, enable the appropriate depth comparison and keep splat depth writes disabled. Conventional Z often uses LessEqual; reverse Z often uses GreaterEqual, paired with the engine’s clear value and projection. Gaussian center-depth testing is an approximation, not exact volumetric intersection with meshes.先绘制不透明几何。为 splat pipeline 配置兼容深度附件,使用正确深度比较,关闭 splat 深度写入。传统 Z 常用 LessEqual,反向 Z 常用 GreaterEqual,必须与引擎清屏值、投影配套。按 Gaussian 中心深度测试是一种近似,不等于与 mesh 精确求体积交。
The render pass sample count and attachment format must match the pipeline. Current StgPass is single-sample. Decide how learned RGB maps into the engine’s linear/HDR working space before blending and tone mapping. Merely changing the attachment to sRGB or HDR does not establish that conversion.Render pass 的采样数、附件格式必须与 pipeline 匹配,当前 StgPass 为单采样。应先确定学到的 RGB 如何映射进引擎线性/HDR 工作空间,再混合和 tone mapping。只把附件换成 sRGB 或 HDR,并不能自动建立正确转换。
4. Write a small host contract4. 写一份小而明确的宿主约定
Record: right-handed view with forward −Z; column-major matrices; WebGPU clip depth 0..1; viewport in physical pixels; normalized model time; declared color convention; matching depth and sample count. These lines are more valuable than a vague “works cross-platform” badge.写下:右手观察空间且朝 −Z;列主序矩阵;WebGPU 裁剪深度 0..1;视口为物理像素;模型时间已归一化;明确色彩约定;深度和采样数兼容。这几行比笼统的“支持跨平台”标签更有价值。
RUN → OBSERVE运行 → 观察
Make this step concrete把这一步运行起来
One native host and one browser host render the same representation. The hidden-window smoke presents stages 0–7 and exits with status.一个桌面宿主和一个浏览器宿主渲染同一表示。隐藏窗口 smoke 会显示提交阶段 0–7,随后退出并返回状态。
Open GPU milestone 7 →打开 GPU 里程碑 7 →cargo run --locked --example workshop -- 7
cargo run --locked --example workshop -- --smoke
cargo check --locked --target wasm32-unknown-unknownCommands use the reference repository. Rust/WGSL changes need a native rebuild or npm run build:wasm, followed by a browser reload.命令以参考仓库为基础。修改 Rust/WGSL 后需重新编译桌面程序,或运行 npm run build:wasm 后刷新浏览器。
IDEA → IMPLEMENTATION想法 → 实现
Follow the running code沿着真实代码看
Excerpts are extracted from the repository at site build time. Expand only the part you are studying; full files include context and line numbers.片段在网站构建时直接从仓库提取。只展开正在研究的部分,完整文件包含上下文与行号。
src/workshop_web.rs · L13–26
The browser creates only its surface before calling the shared host.浏览器创建自己的 surface 后便调用共享宿主。
#[wasm_bindgen(js_name = create)]
pub async fn create(canvas: web_sys::HtmlCanvasElement) -> Result<WorkshopRenderer, JsValue> {
console_error_panic_hook::set_once();
let width = canvas.width();
let height = canvas.height();
let instance = wgpu::Instance::new(wgpu::InstanceDescriptor::new_without_display_handle());
let surface = instance
.create_surface(wgpu::SurfaceTarget::Canvas(canvas))
.map_err(|e| JsValue::from_str(&e.to_string()))?;
let inner = WorkshopSurface::new(instance, surface, width, height)
.await
.map_err(|e| JsValue::from_str(&e))?;
Ok(Self { inner })
}Open full source with line numbers ↗打开带行号的完整源码 ↗examples/workshop.rs · L137–225
OS events are translated into the same input fields.将系统事件转换成相同输入字段。
fn window_event(&mut self, event_loop: &ActiveEventLoop, id: WindowId, event: WindowEvent) {
let Some(window) = self.window.clone() else {
return;
};
if id != window.id() {
return;
}
match event {
WindowEvent::CloseRequested => event_loop.exit(),
WindowEvent::Resized(size) => {
if let Some(gpu) = &mut self.gpu {
gpu.resize(size.width, size.height);
}
window.request_redraw();
}
WindowEvent::KeyboardInput { event, .. }
if event.state == ElementState::Pressed =>
{
match event.logical_key {
Key::Named(NamedKey::Escape) => event_loop.exit(),
Key::Named(NamedKey::Space) => self.playing = !self.playing,
Key::Named(NamedKey::ArrowLeft) => self.frame.yaw -= 0.15,
Key::Named(NamedKey::ArrowRight) => self.frame.yaw += 0.15,
Key::Named(NamedKey::ArrowUp) => {
self.frame.pitch = (self.frame.pitch + 0.1).min(1.4)
}
Key::Named(NamedKey::ArrowDown) => {
self.frame.pitch = (self.frame.pitch - 0.1).max(-1.4)
}
Key::Named(NamedKey::Home) => {
self.frame = Frame {
stage: self.frame.stage,
..Frame::default()
}
}
Key::Character(text) => {
if let Ok(stage @ 0..=7) = text.parse::<u32>() {
if stage == 7 {
if let Err(e) = self.load_model() {
self.fail(event_loop, e);
return;
}
}
self.frame.stage = stage;
}
}
_ => {}
}
window.request_redraw();
}
WindowEvent::RedrawRequested => {
if self.smoke && self.presented >= 8 {
return;
}
let now = Instant::now();
let dt = now.duration_since(self.last).as_secs_f32().min(0.1);
self.last = now;
if self.playing {
self.frame.time = (self.frame.time + dt / 10.0).fract();
}
let size = window.inner_size();
if size.width == 0 || size.height == 0 {
return;
}
if let Some(gpu) = &mut self.gpu {
match gpu.render(self.frame) {
Ok(Some(count)) if self.smoke => {
println!("presented stage={} visible={count}", self.frame.stage);
self.presented += 1;
if self.presented == 8 {
event_loop.exit();
return;
}
self.frame.stage = self.presented;
}
Ok(_) => {}
Err(e) => {
self.fail(event_loop, e);
return;
}
}
}
if self.playing || self.smoke {
window.request_redraw();
}
}
_ => {}
}
}Open full source with line numbers ↗打开带行号的完整源码 ↗Real-model render pass ↗真实模型渲染 pass ↗ · Real-model shader ↗真实模型 shader ↗
Predict it, change it, explain it先预测,再修改,再解释
Your engine uses reverse Z and 4× MSAA. Can you insert the existing StgPass unchanged into that pass?自研引擎使用反向 Z 与 4× MSAA,能直接把现有 StgPass 原封不动插进去吗?
Check your reasoning核对推理
No. Match depth comparison/projection/clear convention, and implement a matching multisample pipeline or use a deliberate single-sample composition stage. Also reconcile color space and attachment format.不能。需要匹配深度比较、投影、清屏约定,并实现兼容的多重采样 pipeline,或有意使用独立的单采样合成阶段。同时核对色彩空间和附件格式。
Keep the theory beside the code把原理放在代码旁边
Open the related derivation or lab ↗打开对应推导或实验 ↗