1use anyhow::{Context, Result};
7
8use super::camera::Camera2D;
9use super::geometry::GeometryBatch;
10use super::postprocess::PostProcessPipeline;
11use super::radiance::RadiancePipeline;
12use super::rendertarget::RenderTargetStore;
13use super::sdf::SdfPipelineStore;
14use super::shader::ShaderStore;
15use super::sprite::SpritePipeline;
16use super::texture::TextureStore;
17
18pub struct TestGpu {
20 pub device: wgpu::Device,
21 pub queue: wgpu::Queue,
22 pub format: wgpu::TextureFormat,
23}
24
25pub struct TestGpuContext {
28 pub device: wgpu::Device,
29 pub queue: wgpu::Queue,
30 pub config: wgpu::SurfaceConfiguration,
31}
32
33impl TestGpuContext {
34 pub fn from_test_gpu(gpu: &TestGpu) -> Self {
36 Self::from_test_gpu_sized(gpu, 64, 64)
37 }
38
39 pub fn from_test_gpu_sized(gpu: &TestGpu, width: u32, height: u32) -> Self {
41 Self {
42 device: gpu.device.clone(),
43 queue: gpu.queue.clone(),
44 config: wgpu::SurfaceConfiguration {
45 usage: wgpu::TextureUsages::RENDER_ATTACHMENT,
46 format: gpu.format,
47 width,
48 height,
49 present_mode: wgpu::PresentMode::AutoVsync,
50 alpha_mode: wgpu::CompositeAlphaMode::Auto,
51 view_formats: vec![],
52 desired_maximum_frame_latency: 2,
53 },
54 }
55 }
56}
57
58impl TestGpu {
59 pub fn new() -> Option<Self> {
62 let instance = wgpu::Instance::new(&wgpu::InstanceDescriptor {
63 backends: wgpu::Backends::all(),
64 ..Default::default()
65 });
66
67 let adapter = pollster::block_on(instance.request_adapter(&wgpu::RequestAdapterOptions {
68 power_preference: wgpu::PowerPreference::default(),
69 compatible_surface: None, force_fallback_adapter: false,
71 }))?;
72
73 let (device, queue) = pollster::block_on(adapter.request_device(
74 &wgpu::DeviceDescriptor {
75 label: Some("test_device"),
76 required_features: wgpu::Features::empty(),
77 required_limits: wgpu::Limits::default(),
78 ..Default::default()
79 },
80 None,
81 ))
82 .ok()?;
83
84 Some(Self {
85 device,
86 queue,
87 format: wgpu::TextureFormat::Rgba8Unorm,
88 })
89 }
90
91 pub fn create_target(&self, width: u32, height: u32) -> TestRenderTarget {
93 let texture = self.device.create_texture(&wgpu::TextureDescriptor {
94 label: Some("test_target"),
95 size: wgpu::Extent3d {
96 width,
97 height,
98 depth_or_array_layers: 1,
99 },
100 mip_level_count: 1,
101 sample_count: 1,
102 dimension: wgpu::TextureDimension::D2,
103 format: self.format,
104 usage: wgpu::TextureUsages::RENDER_ATTACHMENT | wgpu::TextureUsages::COPY_SRC,
105 view_formats: &[],
106 });
107
108 let view = texture.create_view(&wgpu::TextureViewDescriptor::default());
109
110 let bytes_per_row = ((width * 4 + 255) / 256) * 256;
112 let buffer = self.device.create_buffer(&wgpu::BufferDescriptor {
113 label: Some("test_readback"),
114 size: (bytes_per_row * height) as u64,
115 usage: wgpu::BufferUsages::COPY_DST | wgpu::BufferUsages::MAP_READ,
116 mapped_at_creation: false,
117 });
118
119 TestRenderTarget {
120 texture,
121 view,
122 buffer,
123 width,
124 height,
125 bytes_per_row,
126 }
127 }
128
129 pub fn create_sprite_pipeline(&self) -> SpritePipeline {
133 SpritePipeline::new_headless(&self.device, &self.queue, self.format)
134 }
135
136 pub fn create_texture_store(&self) -> TextureStore {
138 TextureStore::new()
139 }
140
141 pub fn create_geometry_batch(&self) -> GeometryBatch {
143 GeometryBatch::new_headless(&self.device, self.format)
144 }
145
146 pub fn create_postprocess(&self) -> PostProcessPipeline {
148 PostProcessPipeline::new_headless(&self.device, self.format)
149 }
150
151 pub fn create_shader_store(&self) -> ShaderStore {
153 ShaderStore::new_headless(&self.device, self.format)
154 }
155
156 pub fn create_render_target_store(&self) -> RenderTargetStore {
158 RenderTargetStore::new()
159 }
160
161 pub fn create_sdf_pipeline(&self) -> SdfPipelineStore {
163 SdfPipelineStore::new_headless(&self.device, self.format)
164 }
165
166 pub fn create_radiance_pipeline(&self) -> RadiancePipeline {
168 RadiancePipeline::new_headless(&self.device, self.format)
169 }
170
171 pub fn create_camera(&self, width: f32, height: f32) -> Camera2D {
174 Camera2D {
175 x: 0.0,
176 y: 0.0,
177 zoom: 1.0,
178 viewport_size: [width, height],
179 ..Camera2D::default()
180 }
181 }
182}
183
184pub struct TestRenderTarget {
186 pub texture: wgpu::Texture,
187 pub view: wgpu::TextureView,
188 buffer: wgpu::Buffer,
189 pub width: u32,
190 pub height: u32,
191 bytes_per_row: u32,
192}
193
194impl TestRenderTarget {
195 pub fn read_pixels(&self, gpu: &TestGpu) -> Result<Vec<u8>> {
198 let mut encoder = gpu.device.create_command_encoder(
199 &wgpu::CommandEncoderDescriptor { label: Some("readback_encoder") },
200 );
201
202 encoder.copy_texture_to_buffer(
203 wgpu::TexelCopyTextureInfo {
204 texture: &self.texture,
205 mip_level: 0,
206 origin: wgpu::Origin3d::ZERO,
207 aspect: wgpu::TextureAspect::All,
208 },
209 wgpu::TexelCopyBufferInfo {
210 buffer: &self.buffer,
211 layout: wgpu::TexelCopyBufferLayout {
212 offset: 0,
213 bytes_per_row: Some(self.bytes_per_row),
214 rows_per_image: Some(self.height),
215 },
216 },
217 wgpu::Extent3d {
218 width: self.width,
219 height: self.height,
220 depth_or_array_layers: 1,
221 },
222 );
223
224 gpu.queue.submit(std::iter::once(encoder.finish()));
225
226 let buffer_slice = self.buffer.slice(..);
228 let (tx, rx) = std::sync::mpsc::channel();
229 buffer_slice.map_async(wgpu::MapMode::Read, move |result| {
230 tx.send(result).unwrap();
231 });
232 gpu.device.poll(wgpu::Maintain::Wait);
233 rx.recv().unwrap().context("Failed to map buffer")?;
234
235 let data = buffer_slice.get_mapped_range();
236
237 let mut pixels = Vec::with_capacity((self.width * self.height * 4) as usize);
239 for y in 0..self.height {
240 let start = (y * self.bytes_per_row) as usize;
241 let end = start + (self.width * 4) as usize;
242 pixels.extend_from_slice(&data[start..end]);
243 }
244
245 drop(data);
246 self.buffer.unmap();
247
248 Ok(pixels)
249 }
250
251 pub fn get_pixel(&self, pixels: &[u8], x: u32, y: u32) -> [u8; 4] {
253 let idx = ((y * self.width + x) * 4) as usize;
254 [pixels[idx], pixels[idx + 1], pixels[idx + 2], pixels[idx + 3]]
255 }
256
257 pub fn pixel_matches(&self, pixels: &[u8], x: u32, y: u32, expected: [u8; 4], tolerance: u8) -> bool {
259 let actual = self.get_pixel(pixels, x, y);
260 actual.iter().zip(expected.iter()).all(|(a, e)| {
261 (*a as i16 - *e as i16).abs() <= tolerance as i16
262 })
263 }
264}
265
266pub fn clear_target(gpu: &TestGpu, target: &TestRenderTarget, color: [f32; 4]) {
268 let mut encoder = gpu.device.create_command_encoder(
269 &wgpu::CommandEncoderDescriptor { label: Some("clear_encoder") },
270 );
271
272 {
273 let _pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
274 label: Some("clear_pass"),
275 color_attachments: &[Some(wgpu::RenderPassColorAttachment {
276 view: &target.view,
277 resolve_target: None,
278 ops: wgpu::Operations {
279 load: wgpu::LoadOp::Clear(wgpu::Color {
280 r: color[0] as f64,
281 g: color[1] as f64,
282 b: color[2] as f64,
283 a: color[3] as f64,
284 }),
285 store: wgpu::StoreOp::Store,
286 },
287 })],
288 depth_stencil_attachment: None,
289 timestamp_writes: None,
290 occlusion_query_set: None,
291 });
292 }
293
294 gpu.queue.submit(std::iter::once(encoder.finish()));
295}
296
297#[cfg(test)]
298mod tests {
299 use super::*;
300
301 #[test]
302 #[ignore] fn test_gpu_context_creation() {
304 let gpu = TestGpu::new().expect("Failed to create GPU context");
305 let _target = gpu.create_target(16, 16);
307 }
308
309 #[test]
310 #[ignore] fn test_clear_and_readback() {
312 let gpu = TestGpu::new().expect("Failed to create GPU context");
313 let target = gpu.create_target(64, 64);
314
315 clear_target(&gpu, &target, [1.0, 0.0, 0.0, 1.0]);
317
318 let pixels = target.read_pixels(&gpu).expect("Failed to read pixels");
319
320 assert!(target.pixel_matches(&pixels, 32, 32, [255, 0, 0, 255], 1));
322 }
323
324 #[test]
325 #[ignore] fn test_clear_to_green() {
327 let gpu = TestGpu::new().expect("Failed to create GPU context");
328 let target = gpu.create_target(32, 32);
329
330 clear_target(&gpu, &target, [0.0, 1.0, 0.0, 1.0]);
331
332 let pixels = target.read_pixels(&gpu).expect("Failed to read pixels");
333 assert!(target.pixel_matches(&pixels, 16, 16, [0, 255, 0, 255], 1));
334 }
335}