Skip to content

Commit 5e65c31

Browse files
committed
feat(ie-render): GPU-accelerated rendering with wgpu #77
Replace softbuffer CPU rendering with wgpu GPU pipeline: GpuRenderer (gpu.rs): - wgpu surface/device/queue initialization from winit window - WGSL vertex+fragment shaders for colored rectangles (rect.wgsl) - Vertex format: position (vec2) + color (vec4), 6 vertices per rect - Uniform buffer for viewport size → pixel-to-clip-space transform - Alpha blending enabled for transparency - resize() reconfigures surface and updates viewport uniform - render() builds vertex buffer from PaintCommands, single draw call - Text rendered as placeholder character rectangles (glyphon in #76) Shell integration (app.rs): - GpuRenderer replaces softbuffer surface - Created via tokio_runtime.block_on() in resumed() handler - render_page() returns Vec<PaintCommand> (not pixels) - paint() passes commands to gpu_renderer.render() - Resized handler calls renderer.resize() - Removed softbuffer dependency from ie-shell Software renderer kept for headless mode and unit tests.
1 parent cc45262 commit 5e65c31

8 files changed

Lines changed: 869 additions & 255 deletions

File tree

Cargo.lock

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

Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,7 @@ serde_json = "1"
5353
clap = { version = "4", features = ["derive"] }
5454
softbuffer = "0.4"
5555
bytes = "1"
56+
bytemuck = { version = "1", features = ["derive"] }
5657
async-trait = "0.1"
5758
chrono = { version = "0.4", features = ["serde"] }
5859
tempfile = "3"

crates/ie-render/Cargo.toml

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
[package]
22
name = "ie-render"
33
version = "0.1.0"
4-
description = "Rendering engine for Internet Exploder — display list and software rasterizer"
4+
description = "Rendering engine for Internet Exploder — GPU-accelerated via wgpu"
55
edition.workspace = true
66
authors.workspace = true
77
license.workspace = true
@@ -11,6 +11,9 @@ repository.workspace = true
1111
anyhow.workspace = true
1212
thiserror.workspace = true
1313
tracing.workspace = true
14+
wgpu.workspace = true
15+
winit.workspace = true
16+
bytemuck.workspace = true
1417
ie-layout.workspace = true
1518
ie-css.workspace = true
1619

crates/ie-render/src/gpu.rs

Lines changed: 320 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,320 @@
1+
use std::sync::Arc;
2+
3+
use anyhow::Result;
4+
use wgpu::util::DeviceExt;
5+
6+
use crate::paint::{Color, PaintCommand};
7+
8+
#[repr(C)]
9+
#[derive(Copy, Clone, bytemuck::Pod, bytemuck::Zeroable)]
10+
struct Vertex {
11+
position: [f32; 2],
12+
color: [f32; 4],
13+
}
14+
15+
#[repr(C)]
16+
#[derive(Copy, Clone, bytemuck::Pod, bytemuck::Zeroable)]
17+
struct Uniforms {
18+
viewport_size: [f32; 2],
19+
_padding: [f32; 2],
20+
}
21+
22+
pub struct GpuRenderer {
23+
surface: wgpu::Surface<'static>,
24+
device: wgpu::Device,
25+
queue: wgpu::Queue,
26+
config: wgpu::SurfaceConfiguration,
27+
pipeline: wgpu::RenderPipeline,
28+
uniform_buffer: wgpu::Buffer,
29+
uniform_bind_group: wgpu::BindGroup,
30+
size: (u32, u32),
31+
}
32+
33+
impl GpuRenderer {
34+
pub async fn new(window: Arc<winit::window::Window>) -> Result<Self> {
35+
let size = window.inner_size();
36+
let instance = wgpu::Instance::new(&wgpu::InstanceDescriptor::default());
37+
let surface = instance.create_surface(window)?;
38+
let adapter = instance
39+
.request_adapter(&wgpu::RequestAdapterOptions {
40+
compatible_surface: Some(&surface),
41+
..Default::default()
42+
})
43+
.await
44+
.ok_or_else(|| anyhow::anyhow!("no suitable GPU adapter"))?;
45+
let (device, queue) = adapter
46+
.request_device(&wgpu::DeviceDescriptor::default(), None)
47+
.await?;
48+
49+
let surface_caps = surface.get_capabilities(&adapter);
50+
let surface_format = surface_caps
51+
.formats
52+
.iter()
53+
.find(|f| f.is_srgb())
54+
.copied()
55+
.unwrap_or(surface_caps.formats[0]);
56+
57+
let config = wgpu::SurfaceConfiguration {
58+
usage: wgpu::TextureUsages::RENDER_ATTACHMENT,
59+
format: surface_format,
60+
width: size.width.max(1),
61+
height: size.height.max(1),
62+
present_mode: wgpu::PresentMode::AutoVsync,
63+
desired_maximum_frame_latency: 2,
64+
alpha_mode: surface_caps.alpha_modes[0],
65+
view_formats: vec![],
66+
};
67+
surface.configure(&device, &config);
68+
69+
let shader = device.create_shader_module(wgpu::ShaderModuleDescriptor {
70+
label: Some("rect shader"),
71+
source: wgpu::ShaderSource::Wgsl(include_str!("shaders/rect.wgsl").into()),
72+
});
73+
74+
let uniforms = Uniforms {
75+
viewport_size: [size.width.max(1) as f32, size.height.max(1) as f32],
76+
_padding: [0.0; 2],
77+
};
78+
let uniform_buffer = device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
79+
label: Some("uniform buffer"),
80+
contents: bytemuck::cast_slice(&[uniforms]),
81+
usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST,
82+
});
83+
84+
let bind_group_layout = device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
85+
label: Some("uniform bind group layout"),
86+
entries: &[wgpu::BindGroupLayoutEntry {
87+
binding: 0,
88+
visibility: wgpu::ShaderStages::VERTEX,
89+
ty: wgpu::BindingType::Buffer {
90+
ty: wgpu::BufferBindingType::Uniform,
91+
has_dynamic_offset: false,
92+
min_binding_size: None,
93+
},
94+
count: None,
95+
}],
96+
});
97+
98+
let uniform_bind_group = device.create_bind_group(&wgpu::BindGroupDescriptor {
99+
label: Some("uniform bind group"),
100+
layout: &bind_group_layout,
101+
entries: &[wgpu::BindGroupEntry {
102+
binding: 0,
103+
resource: uniform_buffer.as_entire_binding(),
104+
}],
105+
});
106+
107+
let pipeline_layout = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
108+
label: Some("pipeline layout"),
109+
bind_group_layouts: &[&bind_group_layout],
110+
push_constant_ranges: &[],
111+
});
112+
113+
let pipeline = device.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
114+
label: Some("rect pipeline"),
115+
layout: Some(&pipeline_layout),
116+
vertex: wgpu::VertexState {
117+
module: &shader,
118+
entry_point: Some("vs_main"),
119+
buffers: &[wgpu::VertexBufferLayout {
120+
array_stride: std::mem::size_of::<Vertex>() as u64,
121+
step_mode: wgpu::VertexStepMode::Vertex,
122+
attributes: &[
123+
wgpu::VertexAttribute {
124+
offset: 0,
125+
shader_location: 0,
126+
format: wgpu::VertexFormat::Float32x2,
127+
},
128+
wgpu::VertexAttribute {
129+
offset: 8,
130+
shader_location: 1,
131+
format: wgpu::VertexFormat::Float32x4,
132+
},
133+
],
134+
}],
135+
compilation_options: Default::default(),
136+
},
137+
fragment: Some(wgpu::FragmentState {
138+
module: &shader,
139+
entry_point: Some("fs_main"),
140+
targets: &[Some(wgpu::ColorTargetState {
141+
format: surface_format,
142+
blend: Some(wgpu::BlendState::ALPHA_BLENDING),
143+
write_mask: wgpu::ColorWrites::ALL,
144+
})],
145+
compilation_options: Default::default(),
146+
}),
147+
primitive: wgpu::PrimitiveState {
148+
topology: wgpu::PrimitiveTopology::TriangleList,
149+
..Default::default()
150+
},
151+
depth_stencil: None,
152+
multisample: wgpu::MultisampleState::default(),
153+
multiview: None,
154+
cache: None,
155+
});
156+
157+
Ok(Self {
158+
surface,
159+
device,
160+
queue,
161+
config,
162+
pipeline,
163+
uniform_buffer,
164+
uniform_bind_group,
165+
size: (size.width.max(1), size.height.max(1)),
166+
})
167+
}
168+
169+
pub fn resize(&mut self, width: u32, height: u32) {
170+
if width == 0 || height == 0 {
171+
return;
172+
}
173+
self.size = (width, height);
174+
self.config.width = width;
175+
self.config.height = height;
176+
self.surface.configure(&self.device, &self.config);
177+
178+
let uniforms = Uniforms {
179+
viewport_size: [width as f32, height as f32],
180+
_padding: [0.0; 2],
181+
};
182+
self.queue
183+
.write_buffer(&self.uniform_buffer, 0, bytemuck::cast_slice(&[uniforms]));
184+
}
185+
186+
pub fn render(&mut self, commands: &[PaintCommand]) -> Result<()> {
187+
let output = self.surface.get_current_texture()?;
188+
let view = output
189+
.texture
190+
.create_view(&wgpu::TextureViewDescriptor::default());
191+
192+
let mut vertices: Vec<Vertex> = Vec::new();
193+
for cmd in commands {
194+
match cmd {
195+
PaintCommand::FillRect {
196+
x,
197+
y,
198+
width,
199+
height,
200+
color,
201+
} => {
202+
push_rect(&mut vertices, *x, *y, *width, *height, color);
203+
}
204+
PaintCommand::Text {
205+
text,
206+
x,
207+
y,
208+
font_size,
209+
color,
210+
} => {
211+
// Placeholder: character rectangles (replaced by glyphon in #76)
212+
let char_w = font_size * 0.5;
213+
let char_h = font_size * 0.7;
214+
let glyph_y = y + (font_size - char_h) * 0.5;
215+
let mut cx = *x;
216+
for ch in text.chars() {
217+
if ch != ' ' {
218+
let glyph_w = char_w * 0.7;
219+
push_rect(&mut vertices, cx, glyph_y, glyph_w, char_h, color);
220+
}
221+
cx += char_w;
222+
}
223+
}
224+
}
225+
}
226+
227+
let vertex_buffer = self
228+
.device
229+
.create_buffer_init(&wgpu::util::BufferInitDescriptor {
230+
label: Some("vertex buffer"),
231+
contents: bytemuck::cast_slice(&vertices),
232+
usage: wgpu::BufferUsages::VERTEX,
233+
});
234+
235+
let mut encoder = self
236+
.device
237+
.create_command_encoder(&wgpu::CommandEncoderDescriptor {
238+
label: Some("render encoder"),
239+
});
240+
241+
{
242+
let mut pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
243+
label: Some("render pass"),
244+
color_attachments: &[Some(wgpu::RenderPassColorAttachment {
245+
view: &view,
246+
resolve_target: None,
247+
ops: wgpu::Operations {
248+
load: wgpu::LoadOp::Clear(wgpu::Color::WHITE),
249+
store: wgpu::StoreOp::Store,
250+
},
251+
})],
252+
depth_stencil_attachment: None,
253+
..Default::default()
254+
});
255+
256+
if !vertices.is_empty() {
257+
pass.set_pipeline(&self.pipeline);
258+
pass.set_bind_group(0, &self.uniform_bind_group, &[]);
259+
pass.set_vertex_buffer(0, vertex_buffer.slice(..));
260+
pass.draw(0..vertices.len() as u32, 0..1);
261+
}
262+
}
263+
264+
self.queue.submit(std::iter::once(encoder.finish()));
265+
output.present();
266+
Ok(())
267+
}
268+
269+
pub fn size(&self) -> (u32, u32) {
270+
self.size
271+
}
272+
273+
pub fn device(&self) -> &wgpu::Device {
274+
&self.device
275+
}
276+
277+
pub fn queue(&self) -> &wgpu::Queue {
278+
&self.queue
279+
}
280+
281+
pub fn surface_format(&self) -> wgpu::TextureFormat {
282+
self.config.format
283+
}
284+
}
285+
286+
fn push_rect(vertices: &mut Vec<Vertex>, x: f32, y: f32, w: f32, h: f32, color: &Color) {
287+
let c = [
288+
color.r as f32 / 255.0,
289+
color.g as f32 / 255.0,
290+
color.b as f32 / 255.0,
291+
color.a as f32 / 255.0,
292+
];
293+
let (x0, y0, x1, y1) = (x, y, x + w, y + h);
294+
vertices.extend_from_slice(&[
295+
Vertex {
296+
position: [x0, y0],
297+
color: c,
298+
},
299+
Vertex {
300+
position: [x1, y0],
301+
color: c,
302+
},
303+
Vertex {
304+
position: [x0, y1],
305+
color: c,
306+
},
307+
Vertex {
308+
position: [x0, y1],
309+
color: c,
310+
},
311+
Vertex {
312+
position: [x1, y0],
313+
color: c,
314+
},
315+
Vertex {
316+
position: [x1, y1],
317+
color: c,
318+
},
319+
]);
320+
}

crates/ie-render/src/lib.rs

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,12 @@
11
//! # ie-render
22
//!
33
//! Rendering engine for Internet Exploder.
4-
//! Currently uses software rendering via softbuffer.
5-
//! GPU rendering (wgpu) will be added later.
4+
//! GPU-accelerated via wgpu, with software fallback for tests.
65
6+
pub mod gpu;
77
pub mod paint;
88
pub mod software;
99

10+
pub use gpu::GpuRenderer;
1011
pub use paint::{Color, PaintCommand, build_display_list};
1112
pub use software::{SoftwareTextMeasure, render_to_buffer};
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
struct VertexInput {
2+
@location(0) position: vec2<f32>,
3+
@location(1) color: vec4<f32>,
4+
};
5+
6+
struct VertexOutput {
7+
@builtin(position) position: vec4<f32>,
8+
@location(0) color: vec4<f32>,
9+
};
10+
11+
struct Uniforms {
12+
viewport_size: vec2<f32>,
13+
};
14+
15+
@group(0) @binding(0)
16+
var<uniform> uniforms: Uniforms;
17+
18+
@vertex
19+
fn vs_main(in: VertexInput) -> VertexOutput {
20+
var out: VertexOutput;
21+
let x = (in.position.x / uniforms.viewport_size.x) * 2.0 - 1.0;
22+
let y = 1.0 - (in.position.y / uniforms.viewport_size.y) * 2.0;
23+
out.position = vec4<f32>(x, y, 0.0, 1.0);
24+
out.color = in.color;
25+
return out;
26+
}
27+
28+
@fragment
29+
fn fs_main(in: VertexOutput) -> @location(0) vec4<f32> {
30+
return in.color;
31+
}

0 commit comments

Comments
 (0)