02 / 12 · Boot the renderer启动渲染器
Connect a window to the GPU把窗口连接到 GPU
Initialize both hosts, handle resizing, and follow a frame’s lifetime without confusing a window with a texture.初始化两个宿主,处理尺寸变化,分清窗口与纹理,追踪一帧的生命周期。
A window is a place on the desktop. A surface is the GPU’s connection to that place. The texture you render into changes from frame to frame. Keeping a texture forever is like writing every letter onto yesterday’s envelope.窗口是桌面上的一个区域,surface 是 GPU 通向这个区域的连接。每帧渲染的 texture 会变。把一张显示纹理永远留着,就像每封信都写在昨天的信封上。
1. Ask for a compatible device1. 请求兼容的设备
Instance discovers graphics backends. Adapter selects a GPU compatible with our surface. Device creates resources; Queue uploads data and submits command buffers. Request the capabilities you need, not every optional feature the GPU exposes. This workshop uses baseline rendering and storage buffers.Instance 负责发现图形后端,Adapter 选择兼容 surface 的 GPU,Device 创建资源,Queue 上传数据并提交命令缓冲。只请求真正需要的能力,不把 GPU 的全部可选功能都打开。本实验使用基础渲染能力及 storage buffer。
The attachment format is part of a pipeline’s contract. Query the surface formats before creating the pipeline. This series prefers a non-sRGB UNORM format to match the baseline’s numeric RGB convention; production color management needs an explicit source-color decision. Recreating a surface with a different format requires a compatible pipeline.附件格式是 pipeline 约定的一部分。先查询 surface 支持的格式,再创建 pipeline。本系列优先选非 sRGB 的 UNORM 格式,以匹配基线的 RGB 数值约定;生产色彩管理还需要明确来源色彩空间。新 surface 若换了格式,pipeline 也必须兼容。
2. Create the host at the right moment2. 在合适的生命周期创建宿主
The native app creates its window inside resumed. It passes an Arc clone into create_surface so the surface can keep the window alive. On desktop only, pollster waits for asynchronous initialization. The browser instead awaits WorkshopRenderer.create(canvas); blocking that event loop would prevent the page from progressing.桌面应用在 resumed 回调中创建窗口,把 Arc 的克隆传给 create_surface,让 surface 能保持窗口存活。只在桌面启动时使用 pollster 等待异步初始化。浏览器则 await WorkshopRenderer.create(canvas);阻塞浏览器事件循环会让页面无法继续。
On suspend, the sample drops the native GPU/surface state and window, then reconstructs them on resume. A production app would also preserve UI state and implement device-loss recovery. Here an unrecoverable validation failure stops with an error and a reload path; the lesson does not claim full recovery.暂停宿主时,示例释放桌面 GPU/surface 状态和窗口,恢复时重建。生产应用还应保存界面状态并实现设备丢失恢复。本实验遇到不可恢复的验证错误会停止、显示错误并允许刷新,不声称已经实现完整恢复。
3. Size is measured in two units3. 尺寸有两套单位
A canvas may occupy 600 × 400 CSS pixels while its backing texture is 900 × 600 physical pixels at a capped device-pixel ratio of 1.5. Set both canvas dimensions and the surface configuration. Compute the camera aspect ratio from the actual render width and height. Never configure a zero-sized surface while a native window is minimized.Canvas 可能在界面占 600 × 400 CSS 像素,而在限制为 1.5 的设备像素比下使用 900 × 600 物理像素纹理。Canvas 尺寸与 surface 配置都要更新。相机宽高比取实际渲染宽高。桌面窗口最小化时,不能拿零尺寸配置 surface。
4. Surface errors are different events4. Surface 错误不是同一种情况
Outdated or Lost triggers surface reconfiguration; Timeout or Occluded skips this frame. Validation stops the sample and reports the problem. Only a successfully acquired texture gets presented. Notice that wgpu 30 uses queue.present(frame); old code using a different API is not a reason to skip checking the version.Outdated 或 Lost 时重新配置 surface;Timeout 或 Occluded 时跳过这一帧;Validation 时停止示例并报告问题。只有成功获取的纹理才会被显示。注意 wgpu 30 使用 queue.present(frame),遇到旧教程的不同 API 时应先检查版本。
RUN → OBSERVE运行 → 观察
Make this step concrete把这一步运行起来
The same clear screen in a native window and browser canvas. Resize each; the reported render resolution should track the backing surface.桌面窗口与浏览器 canvas 显示相同的清屏。分别调整尺寸,渲染分辨率应随底层 surface 改变。
Open GPU milestone 0 →打开 GPU 里程碑 0 →rustup target add wasm32-unknown-unknown
cargo install wasm-pack --locked
npm ci
npm run dev
# Open the local URL printed by Wrangler, then /build/lab/?stage=0Commands 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_surface.rs · L17–70
GPU setup is shared; the host supplies the surface.GPU 初始化共用,宿主只提供 surface。
pub async fn new(
instance: wgpu::Instance,
surface: wgpu::Surface<'static>,
width: u32,
height: u32,
) -> Result<Self, String> {
let adapter = instance
.request_adapter(&wgpu::RequestAdapterOptions {
power_preference: wgpu::PowerPreference::HighPerformance,
compatible_surface: Some(&surface),
force_fallback_adapter: false,
apply_limit_buckets: false,
})
.await
.map_err(|e| e.to_string())?;
let (device, queue) = adapter
.request_device(&wgpu::DeviceDescriptor::default())
.await
.map_err(|e| e.to_string())?;
let caps = surface.get_capabilities(&adapter);
// Keep the educational RGB values in the same UNORM convention as the
// research baseline. A production HDR renderer needs an explicit conversion.
let format = caps
.formats
.iter()
.copied()
.find(|f| !f.is_srgb())
.unwrap_or(caps.formats[0]);
let config = wgpu::SurfaceConfiguration {
usage: wgpu::TextureUsages::RENDER_ATTACHMENT,
format,
color_space: wgpu::SurfaceColorSpace::Auto,
width: 1,
height: 1,
present_mode: wgpu::PresentMode::Fifo,
alpha_mode: caps.alpha_modes[0],
view_formats: vec![],
desired_maximum_frame_latency: 2,
};
let pass = WorkshopPass::new(&device, format);
let info = adapter.get_info();
let adapter_label = format!("{:?} · {}", info.backend, info.name);
let mut renderer = Self {
surface,
device,
queue,
config,
pass,
adapter_label,
relight: None,
};
renderer.resize(width.max(1), height.max(1));
Ok(renderer)
}Open full source with line numbers ↗打开带行号的完整源码 ↗examples/workshop.rs · L77–130
The native window lives outside the pass.桌面窗口位于渲染 pass 之外。
fn resumed(&mut self, event_loop: &ActiveEventLoop) {
if self.window.is_some() {
return;
}
let attributes = Window::default_attributes()
.with_title("4DGS Workshop · 0–7 stages · arrows orbit · Space pause · Home reset")
.with_inner_size(winit::dpi::LogicalSize::new(960, 640))
.with_visible(!self.smoke);
let window = match event_loop.create_window(attributes) {
Ok(window) => Arc::new(window),
Err(e) => {
self.fail(event_loop, e.to_string());
return;
}
};
let mut descriptor = wgpu::InstanceDescriptor::new_without_display_handle();
if cfg!(target_os = "windows") {
descriptor.backends = wgpu::Backends::DX12;
}
let instance = wgpu::Instance::new(descriptor);
let surface = match instance.create_surface(window.clone()) {
Ok(surface) => surface,
Err(e) => {
self.fail(event_loop, e.to_string());
return;
}
};
let size = window.inner_size();
// Blocking is confined to desktop startup. The browser host awaits.
self.gpu = match pollster::block_on(WorkshopSurface::new(
instance,
surface,
size.width,
size.height,
)) {
Ok(gpu) => {
println!("{}", gpu.adapter_label);
Some(gpu)
}
Err(e) => {
self.fail(event_loop, e);
return;
}
};
self.window = Some(window);
if self.frame.stage == 7 || self.smoke {
if let Err(e) = self.load_model() {
self.fail(event_loop, e);
return;
}
}
self.last = Instant::now();
self.window.as_ref().unwrap().request_redraw();
}Open full source with line numbers ↗打开带行号的完整源码 ↗Real-model render pass ↗真实模型渲染 pass ↗ · Real-model shader ↗真实模型 shader ↗
Predict it, change it, explain it先预测,再修改,再解释
Minimize and restore the native window. Then resize the browser lab to a narrow viewport. Which code should run, and which values must never be zero?最小化并恢复桌面窗口,再把浏览器实验改成窄窗口。哪些代码应该执行,哪些配置值不能为零?
Check your reasoning核对推理
Resize updates the surface configuration and next frame’s projection. Native rendering skips a zero-sized window. Width and height passed to configure or prepare must be nonzero. CSS size alone does not resize GPU attachments.Resize 更新 surface 配置以及下一帧的投影。桌面渲染会跳过零尺寸窗口。传给 configure 或 prepare 的宽高必须非零,只修改 CSS 大小不会调整 GPU 附件。
Keep the theory beside the code把原理放在代码旁边
Open the related derivation or lab ↗打开对应推导或实验 ↗