Skip to main content

oxide_browser/
gpu.rs

1//! WebGPU-style GPU resource management for guest wasm modules.
2//!
3//! This module implements a sandboxed GPU API inspired by the WebGPU specification.
4//! Guest modules can create buffers, textures, shaders, and pipelines, then submit
5//! draw calls or compute dispatches — all mediated through the host capability system.
6//!
7//! Resources are identified by opaque `u32` handles; the host owns the actual `wgpu`
8//! objects and enforces limits (maximum buffer size, texture dimensions, shader
9//! compilation timeouts, etc.).
10
11use std::collections::HashMap;
12use std::sync::Arc;
13use wgpu;
14
15/// Opaque handle type for GPU resources visible to the guest.
16pub type GpuHandle = u32;
17
18/// Per-module GPU state: device, queue, and resource tables.
19pub struct GpuState {
20    device: Arc<wgpu::Device>,
21    queue: Arc<wgpu::Queue>,
22    next_handle: GpuHandle,
23    buffers: HashMap<GpuHandle, wgpu::Buffer>,
24    textures: HashMap<GpuHandle, GpuTexture>,
25    shaders: HashMap<GpuHandle, wgpu::ShaderModule>,
26    pipelines: HashMap<GpuHandle, GpuPipeline>,
27    /// RGBA output surface that gets composited into the canvas (reserved for GPU readback).
28    #[allow(dead_code)]
29    readback_buffer: Option<ReadbackBuffer>,
30}
31
32#[allow(dead_code)]
33struct GpuTexture {
34    texture: wgpu::Texture,
35    view: wgpu::TextureView,
36    width: u32,
37    height: u32,
38}
39
40enum GpuPipeline {
41    Render(wgpu::RenderPipeline),
42    Compute(wgpu::ComputePipeline),
43}
44
45#[allow(dead_code)]
46struct ReadbackBuffer {
47    buffer: wgpu::Buffer,
48    width: u32,
49    height: u32,
50}
51
52/// Maximum buffer size a guest may allocate (64 MB).
53const MAX_BUFFER_SIZE: u64 = 64 * 1024 * 1024;
54
55/// Maximum texture dimension (4096).
56const MAX_TEXTURE_DIM: u32 = 4096;
57
58impl GpuState {
59    fn alloc_handle(&mut self) -> GpuHandle {
60        let h = self.next_handle;
61        self.next_handle += 1;
62        h
63    }
64
65    /// Create a GPU buffer with the given size and usage flags.
66    /// Returns a handle, or 0 on failure.
67    pub fn create_buffer(&mut self, size: u64, usage_bits: u32) -> GpuHandle {
68        if size == 0 || size > MAX_BUFFER_SIZE {
69            return 0;
70        }
71        let usage = wgpu::BufferUsages::from_bits_truncate(usage_bits)
72            | wgpu::BufferUsages::COPY_DST
73            | wgpu::BufferUsages::COPY_SRC;
74        let buffer = self.device.create_buffer(&wgpu::BufferDescriptor {
75            label: Some("oxide_guest_buffer"),
76            size,
77            usage,
78            mapped_at_creation: false,
79        });
80        let h = self.alloc_handle();
81        self.buffers.insert(h, buffer);
82        h
83    }
84
85    /// Create a 2D RGBA8 texture. Returns a handle, or 0 on failure.
86    pub fn create_texture(&mut self, width: u32, height: u32) -> GpuHandle {
87        if width == 0 || height == 0 || width > MAX_TEXTURE_DIM || height > MAX_TEXTURE_DIM {
88            return 0;
89        }
90        let texture = self.device.create_texture(&wgpu::TextureDescriptor {
91            label: Some("oxide_guest_texture"),
92            size: wgpu::Extent3d {
93                width,
94                height,
95                depth_or_array_layers: 1,
96            },
97            mip_level_count: 1,
98            sample_count: 1,
99            dimension: wgpu::TextureDimension::D2,
100            format: wgpu::TextureFormat::Rgba8UnormSrgb,
101            usage: wgpu::TextureUsages::TEXTURE_BINDING
102                | wgpu::TextureUsages::COPY_DST
103                | wgpu::TextureUsages::RENDER_ATTACHMENT,
104            view_formats: &[],
105        });
106        let view = texture.create_view(&wgpu::TextureViewDescriptor::default());
107        let h = self.alloc_handle();
108        self.textures.insert(
109            h,
110            GpuTexture {
111                texture,
112                view,
113                width,
114                height,
115            },
116        );
117        h
118    }
119
120    /// Compile a WGSL shader module. Returns a handle, or 0 on failure.
121    pub fn create_shader(&mut self, source: &str) -> GpuHandle {
122        let module = self
123            .device
124            .create_shader_module(wgpu::ShaderModuleDescriptor {
125                label: Some("oxide_guest_shader"),
126                source: wgpu::ShaderSource::Wgsl(source.into()),
127            });
128        let h = self.alloc_handle();
129        self.shaders.insert(h, module);
130        h
131    }
132
133    /// Create a render pipeline from a shader handle.
134    /// `vertex_entry` and `fragment_entry` name the WGSL entry points.
135    /// Returns a handle, or 0 if the shader handle is invalid.
136    pub fn create_render_pipeline(
137        &mut self,
138        shader_handle: GpuHandle,
139        vertex_entry: &str,
140        fragment_entry: &str,
141    ) -> GpuHandle {
142        let shader = match self.shaders.get(&shader_handle) {
143            Some(s) => s,
144            None => return 0,
145        };
146        let pipeline = self
147            .device
148            .create_render_pipeline(&wgpu::RenderPipelineDescriptor {
149                label: Some("oxide_guest_render_pipeline"),
150                layout: None,
151                vertex: wgpu::VertexState {
152                    module: shader,
153                    entry_point: Some(vertex_entry),
154                    compilation_options: Default::default(),
155                    buffers: &[],
156                },
157                fragment: Some(wgpu::FragmentState {
158                    module: shader,
159                    entry_point: Some(fragment_entry),
160                    compilation_options: Default::default(),
161                    targets: &[Some(wgpu::ColorTargetState {
162                        format: wgpu::TextureFormat::Rgba8UnormSrgb,
163                        blend: Some(wgpu::BlendState::ALPHA_BLENDING),
164                        write_mask: wgpu::ColorWrites::ALL,
165                    })],
166                }),
167                primitive: wgpu::PrimitiveState {
168                    topology: wgpu::PrimitiveTopology::TriangleList,
169                    ..Default::default()
170                },
171                depth_stencil: None,
172                multisample: wgpu::MultisampleState::default(),
173                multiview: None,
174                cache: None,
175            });
176        let h = self.alloc_handle();
177        self.pipelines.insert(h, GpuPipeline::Render(pipeline));
178        h
179    }
180
181    /// Create a compute pipeline from a shader handle.
182    pub fn create_compute_pipeline(
183        &mut self,
184        shader_handle: GpuHandle,
185        entry_point: &str,
186    ) -> GpuHandle {
187        let shader = match self.shaders.get(&shader_handle) {
188            Some(s) => s,
189            None => return 0,
190        };
191        let pipeline = self
192            .device
193            .create_compute_pipeline(&wgpu::ComputePipelineDescriptor {
194                label: Some("oxide_guest_compute_pipeline"),
195                layout: None,
196                module: shader,
197                entry_point: Some(entry_point),
198                compilation_options: Default::default(),
199                cache: None,
200            });
201        let h = self.alloc_handle();
202        self.pipelines.insert(h, GpuPipeline::Compute(pipeline));
203        h
204    }
205
206    /// Write data to a GPU buffer from guest memory.
207    pub fn write_buffer(&self, handle: GpuHandle, offset: u64, data: &[u8]) -> bool {
208        match self.buffers.get(&handle) {
209            Some(buf) => {
210                self.queue.write_buffer(buf, offset, data);
211                true
212            }
213            None => false,
214        }
215    }
216
217    /// Submit a render pass that draws `vertex_count` vertices using the given pipeline,
218    /// targeting a texture.
219    pub fn draw(
220        &self,
221        pipeline_handle: GpuHandle,
222        target_texture: GpuHandle,
223        vertex_count: u32,
224        instance_count: u32,
225    ) -> bool {
226        let pipeline = match self.pipelines.get(&pipeline_handle) {
227            Some(GpuPipeline::Render(p)) => p,
228            _ => return false,
229        };
230        let target = match self.textures.get(&target_texture) {
231            Some(t) => t,
232            None => return false,
233        };
234
235        let mut encoder = self
236            .device
237            .create_command_encoder(&wgpu::CommandEncoderDescriptor {
238                label: Some("oxide_guest_draw"),
239            });
240
241        {
242            let mut pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
243                label: Some("oxide_guest_render_pass"),
244                color_attachments: &[Some(wgpu::RenderPassColorAttachment {
245                    view: &target.view,
246                    resolve_target: None,
247                    ops: wgpu::Operations {
248                        load: wgpu::LoadOp::Load,
249                        store: wgpu::StoreOp::Store,
250                    },
251                })],
252                depth_stencil_attachment: None,
253                timestamp_writes: None,
254                occlusion_query_set: None,
255            });
256            pass.set_pipeline(pipeline);
257            pass.draw(0..vertex_count, 0..instance_count.max(1));
258        }
259
260        self.queue.submit(std::iter::once(encoder.finish()));
261        true
262    }
263
264    /// Submit a compute dispatch.
265    pub fn dispatch_compute(
266        &self,
267        pipeline_handle: GpuHandle,
268        workgroups_x: u32,
269        workgroups_y: u32,
270        workgroups_z: u32,
271    ) -> bool {
272        let pipeline = match self.pipelines.get(&pipeline_handle) {
273            Some(GpuPipeline::Compute(p)) => p,
274            _ => return false,
275        };
276
277        let mut encoder = self
278            .device
279            .create_command_encoder(&wgpu::CommandEncoderDescriptor {
280                label: Some("oxide_guest_compute"),
281            });
282
283        {
284            let mut pass = encoder.begin_compute_pass(&wgpu::ComputePassDescriptor {
285                label: Some("oxide_guest_compute_pass"),
286                timestamp_writes: None,
287            });
288            pass.set_pipeline(pipeline);
289            pass.dispatch_workgroups(workgroups_x, workgroups_y, workgroups_z);
290        }
291
292        self.queue.submit(std::iter::once(encoder.finish()));
293        true
294    }
295
296    /// Destroy a buffer resource.
297    pub fn destroy_buffer(&mut self, handle: GpuHandle) -> bool {
298        if let Some(buf) = self.buffers.remove(&handle) {
299            buf.destroy();
300            true
301        } else {
302            false
303        }
304    }
305
306    /// Destroy a texture resource.
307    pub fn destroy_texture(&mut self, handle: GpuHandle) -> bool {
308        if let Some(tex) = self.textures.remove(&handle) {
309            tex.texture.destroy();
310            true
311        } else {
312            false
313        }
314    }
315}
316
317/// Initialise the wgpu device and queue, returning a ready-to-use [`GpuState`].
318///
319/// Uses the default backend (Vulkan, Metal, DX12) with low power preference.
320/// Returns `None` if no suitable adapter is found.
321pub fn init_gpu() -> Option<GpuState> {
322    let instance = wgpu::Instance::new(&wgpu::InstanceDescriptor {
323        backends: wgpu::Backends::all(),
324        ..Default::default()
325    });
326
327    let adapter = pollster::block_on(instance.request_adapter(&wgpu::RequestAdapterOptions {
328        power_preference: wgpu::PowerPreference::LowPower,
329        compatible_surface: None,
330        force_fallback_adapter: false,
331    }))
332    .ok()?;
333
334    let (device, queue) = pollster::block_on(adapter.request_device(&wgpu::DeviceDescriptor {
335        label: Some("oxide_gpu"),
336        required_features: wgpu::Features::empty(),
337        required_limits: wgpu::Limits::downlevel_defaults(),
338        memory_hints: Default::default(),
339        trace: wgpu::Trace::Off,
340    }))
341    .ok()?;
342
343    Some(GpuState {
344        device: Arc::new(device),
345        queue: Arc::new(queue),
346        next_handle: 1,
347        buffers: HashMap::new(),
348        textures: HashMap::new(),
349        shaders: HashMap::new(),
350        pipelines: HashMap::new(),
351        readback_buffer: None,
352    })
353}