onedrop_renderer/
gpu_context.rs

1//! GPU context management.
2
3use crate::chain_textures::ChainTextures;
4use crate::config::RenderConfig;
5use crate::error::Result;
6use crate::noise::{NoisePack, NoiseTexture};
7use std::sync::Arc;
8
9/// Shared GPU resources: device + queue + the procedural noise pack that
10/// every chain samples from. Per-chain feedback textures (`render`, `prev`,
11/// `final`, `blur1/2/3`, `blur_scratch`) used to live here too; they have
12/// moved to [`ChainTextures`] so each rendering chain can own its own
13/// feedback loop without crossing the streams. The primary chain's textures
14/// are still owned by `GpuContext` for convenience — secondary chains during
15/// transitions own their own [`ChainTextures`] inside [`crate::RenderChain`].
16pub struct GpuContext {
17    /// WGPU device.
18    pub device: Arc<wgpu::Device>,
19
20    /// Command queue.
21    pub queue: Arc<wgpu::Queue>,
22
23    /// Render configuration (resolution, format).
24    pub config: RenderConfig,
25
26    // ---- Procedural noise pack ----
27    //
28    // Five textures with fixed sizes generated once at startup from a
29    // deterministic seed. Resolution-independent — unaffected by `resize()`.
30    pub noise_lq_texture: wgpu::Texture,
31    pub noise_lq_view: wgpu::TextureView,
32    pub noise_mq_texture: wgpu::Texture,
33    pub noise_mq_view: wgpu::TextureView,
34    pub noise_hq_texture: wgpu::Texture,
35    pub noise_hq_view: wgpu::TextureView,
36    pub noisevol_lq_texture: wgpu::Texture,
37    pub noisevol_lq_view: wgpu::TextureView,
38    pub noisevol_hq_texture: wgpu::Texture,
39    pub noisevol_hq_view: wgpu::TextureView,
40    /// `vec4(w, h, 1/w, 1/h)` per texture, available to the wrapper as
41    /// `texsize_<name>` constants without going through a uniform buffer.
42    pub noise_texsizes: NoiseTexsizes,
43}
44
45/// `texsize_<name>` values for every noise texture in the pack. Real MD2
46/// presets read these as plain `vec4<f32>` and the values never change after
47/// init (the textures are not resized), so we compute them once and emit
48/// them as WGSL constants in the comp wrapper.
49#[derive(Debug, Clone, Copy)]
50pub struct NoiseTexsizes {
51    pub noise_lq: [f32; 4],
52    pub noise_mq: [f32; 4],
53    pub noise_hq: [f32; 4],
54    pub noisevol_lq: [f32; 4],
55    pub noisevol_hq: [f32; 4],
56}
57
58impl GpuContext {
59    /// Create a new GPU context.
60    pub async fn new(config: RenderConfig) -> Result<Self> {
61        let instance = wgpu::Instance::new(wgpu::InstanceDescriptor {
62            backends: wgpu::Backends::all(),
63            ..wgpu::InstanceDescriptor::new_without_display_handle()
64        });
65
66        let adapter = instance
67            .request_adapter(&wgpu::RequestAdapterOptions {
68                power_preference: wgpu::PowerPreference::HighPerformance,
69                compatible_surface: None,
70                force_fallback_adapter: false,
71            })
72            .await?;
73
74        // Raise `max_sampled_textures_per_shader_stage` above the wgpu
75        // default (16) — the comp pass binds 9 built-in sampled textures
76        // (main + 3 blur + 3 noise 2D + 2 noise 3D) plus up to 8 user
77        // textures = 17 in the worst case. 24 leaves a small cushion.
78        let (device, queue) = adapter
79            .request_device(&wgpu::DeviceDescriptor {
80                label: Some("Milkdrop Device"),
81                required_features: wgpu::Features::empty(),
82                required_limits: wgpu::Limits {
83                    max_sampled_textures_per_shader_stage: 24,
84                    ..wgpu::Limits::default()
85                },
86                memory_hints: Default::default(),
87                experimental_features: wgpu::ExperimentalFeatures::default(),
88                trace: wgpu::Trace::Off,
89            })
90            .await?;
91
92        let device = Arc::new(device);
93        let queue = Arc::new(queue);
94
95        Ok(Self::build(device, queue, config))
96    }
97
98    /// Create a GPU context from an existing device and queue. Used when
99    /// sharing one GPU device across multiple components (e.g. the GUI
100    /// passes its surface device into the engine).
101    pub fn from_device(
102        device: Arc<wgpu::Device>,
103        queue: Arc<wgpu::Queue>,
104        config: RenderConfig,
105    ) -> Self {
106        Self::build(device, queue, config)
107    }
108
109    fn build(device: Arc<wgpu::Device>, queue: Arc<wgpu::Queue>, config: RenderConfig) -> Self {
110        let noise = NoisePack::generate();
111        let (noise_lq_texture, noise_lq_view) =
112            Self::make_noise_2d(&device, &queue, &noise.noise_lq, "Noise LQ");
113        let (noise_mq_texture, noise_mq_view) =
114            Self::make_noise_2d(&device, &queue, &noise.noise_mq, "Noise MQ");
115        let (noise_hq_texture, noise_hq_view) =
116            Self::make_noise_2d(&device, &queue, &noise.noise_hq, "Noise HQ");
117        let (noisevol_lq_texture, noisevol_lq_view) =
118            Self::make_noise_3d(&device, &queue, &noise.noisevol_lq, "NoiseVol LQ");
119        let (noisevol_hq_texture, noisevol_hq_view) =
120            Self::make_noise_3d(&device, &queue, &noise.noisevol_hq, "NoiseVol HQ");
121        let noise_texsizes = NoiseTexsizes {
122            noise_lq: noise.noise_lq.texsize(),
123            noise_mq: noise.noise_mq.texsize(),
124            noise_hq: noise.noise_hq.texsize(),
125            noisevol_lq: noise.noisevol_lq.texsize(),
126            noisevol_hq: noise.noisevol_hq.texsize(),
127        };
128
129        Self {
130            device,
131            queue,
132            config,
133            noise_lq_texture,
134            noise_lq_view,
135            noise_mq_texture,
136            noise_mq_view,
137            noise_hq_texture,
138            noise_hq_view,
139            noisevol_lq_texture,
140            noisevol_lq_view,
141            noisevol_hq_texture,
142            noisevol_hq_view,
143            noise_texsizes,
144        }
145    }
146
147    /// Allocate an immutable 2D `Rgba8Unorm` noise texture and upload its
148    /// bytes. Used for `noise_lq` / `noise_mq` / `noise_hq` — small fixed-
149    /// resolution textures that live for the program's lifetime and are
150    /// never re-rendered.
151    fn make_noise_2d(
152        device: &wgpu::Device,
153        queue: &wgpu::Queue,
154        n: &NoiseTexture,
155        label: &str,
156    ) -> (wgpu::Texture, wgpu::TextureView) {
157        let tex = device.create_texture(&wgpu::TextureDescriptor {
158            label: Some(label),
159            size: wgpu::Extent3d {
160                width: n.width,
161                height: n.height,
162                depth_or_array_layers: 1,
163            },
164            mip_level_count: 1,
165            sample_count: 1,
166            dimension: wgpu::TextureDimension::D2,
167            format: wgpu::TextureFormat::Rgba8Unorm,
168            usage: wgpu::TextureUsages::TEXTURE_BINDING | wgpu::TextureUsages::COPY_DST,
169            view_formats: &[],
170        });
171        queue.write_texture(
172            wgpu::TexelCopyTextureInfo {
173                texture: &tex,
174                mip_level: 0,
175                origin: wgpu::Origin3d::ZERO,
176                aspect: wgpu::TextureAspect::All,
177            },
178            &n.bytes,
179            wgpu::TexelCopyBufferLayout {
180                offset: 0,
181                bytes_per_row: Some(n.width * 4),
182                rows_per_image: Some(n.height),
183            },
184            wgpu::Extent3d {
185                width: n.width,
186                height: n.height,
187                depth_or_array_layers: 1,
188            },
189        );
190        let view = tex.create_view(&wgpu::TextureViewDescriptor::default());
191        (tex, view)
192    }
193
194    /// Allocate an immutable 3D `Rgba8Unorm` noise texture (volume).
195    fn make_noise_3d(
196        device: &wgpu::Device,
197        queue: &wgpu::Queue,
198        n: &NoiseTexture,
199        label: &str,
200    ) -> (wgpu::Texture, wgpu::TextureView) {
201        let tex = device.create_texture(&wgpu::TextureDescriptor {
202            label: Some(label),
203            size: wgpu::Extent3d {
204                width: n.width,
205                height: n.height,
206                depth_or_array_layers: n.depth,
207            },
208            mip_level_count: 1,
209            sample_count: 1,
210            dimension: wgpu::TextureDimension::D3,
211            format: wgpu::TextureFormat::Rgba8Unorm,
212            usage: wgpu::TextureUsages::TEXTURE_BINDING | wgpu::TextureUsages::COPY_DST,
213            view_formats: &[],
214        });
215        queue.write_texture(
216            wgpu::TexelCopyTextureInfo {
217                texture: &tex,
218                mip_level: 0,
219                origin: wgpu::Origin3d::ZERO,
220                aspect: wgpu::TextureAspect::All,
221            },
222            &n.bytes,
223            wgpu::TexelCopyBufferLayout {
224                offset: 0,
225                bytes_per_row: Some(n.width * 4),
226                rows_per_image: Some(n.height),
227            },
228            wgpu::Extent3d {
229                width: n.width,
230                height: n.height,
231                depth_or_array_layers: n.depth,
232            },
233        );
234        let view = tex.create_view(&wgpu::TextureViewDescriptor {
235            dimension: Some(wgpu::TextureViewDimension::D3),
236            ..Default::default()
237        });
238        (tex, view)
239    }
240
241    /// Bundle the comp-pass auxiliary texture views for the given chain
242    /// (blur pyramid + noise pack + previous-frame display) into a borrow
243    /// group. The chain provides the per-chain views (blur + prev); this
244    /// context provides the shared noise views.
245    pub fn comp_aux_views_for<'a>(
246        &'a self,
247        chain: &'a ChainTextures,
248    ) -> crate::comp_pipeline::CompAuxViews<'a> {
249        crate::comp_pipeline::CompAuxViews {
250            blur1: &chain.blur1_texture_view,
251            blur2: &chain.blur2_texture_view,
252            blur3: &chain.blur3_texture_view,
253            noise_lq: &self.noise_lq_view,
254            noise_mq: &self.noise_mq_view,
255            noise_hq: &self.noise_hq_view,
256            noisevol_lq: &self.noisevol_lq_view,
257            noisevol_hq: &self.noisevol_hq_view,
258            prev_main: &chain.prev_texture_view,
259        }
260    }
261
262    /// Update the recorded resolution; the renderer is responsible for
263    /// re-allocating each chain's [`ChainTextures`] separately.
264    pub fn set_resolution(&mut self, width: u32, height: u32) {
265        self.config.width = width;
266        self.config.height = height;
267    }
268
269    /// Get aspect ratio.
270    pub fn aspect_ratio(&self) -> f32 {
271        self.config.width as f32 / self.config.height as f32
272    }
273}
274
275#[cfg(test)]
276mod tests {
277    use super::*;
278
279    #[test]
280    fn test_gpu_context_creation() {
281        let config = RenderConfig::default();
282        let context = pollster::block_on(GpuContext::new(config));
283        assert!(context.is_ok());
284    }
285
286    #[test]
287    fn test_aspect_ratio() {
288        let config = RenderConfig {
289            width: 1920,
290            height: 1080,
291            ..Default::default()
292        };
293        let context = pollster::block_on(GpuContext::new(config)).unwrap();
294        assert!((context.aspect_ratio() - 16.0 / 9.0).abs() < 0.01);
295    }
296}