1use std::sync::Arc;
14
15use serde_json::Value;
16use wgpu::util::DeviceExt;
17
18pub const SAMPLE_COUNT: u32 = 4;
19const DEPTH_FORMAT: wgpu::TextureFormat = wgpu::TextureFormat::Depth24Plus;
20
21pub const MAX_TEXTURE_SIZE: u32 = 8192;
23pub const OFFSET_ALIGNMENT: u32 = 256;
25
26enum Obj {
27 Shader(wgpu::ShaderModule),
28 Buffer(wgpu::Buffer),
29 Pipeline(wgpu::RenderPipeline),
30 BindGroup(wgpu::BindGroup),
31 Layout(wgpu::BindGroupLayout),
32 Texture(wgpu::Texture, wgpu::TextureView),
33 Sampler(wgpu::Sampler),
34 Null,
35}
36
37#[derive(Clone)]
39enum Meta {
40 Shader,
41 Buffer { size: u64, usage: u32 },
42 Pipeline,
43 BindGroup { dynamic: Vec<DynEntry> },
45 Layout { entries: Vec<LayoutEntry> },
46 Texture { width: u32, height: u32, mips: u32 },
47 Sampler,
48}
49
50impl Meta {
51 fn kind(&self) -> &'static str {
52 match self {
53 Meta::Shader => "shader",
54 Meta::Buffer { .. } => "buffer",
55 Meta::Pipeline => "pipeline",
56 Meta::BindGroup { .. } => "bind group",
57 Meta::Layout { .. } => "bind group layout",
58 Meta::Texture { .. } => "texture",
59 Meta::Sampler => "sampler",
60 }
61 }
62}
63
64#[derive(Clone, Copy)]
65struct DynEntry {
66 buffer_size: u64,
67 offset: u64,
68 size: u64,
69}
70
71#[derive(Clone, Copy, PartialEq)]
72enum Slot {
73 Uniform,
74 Storage,
75 Texture,
76 Sampler,
77}
78
79#[derive(Clone)]
80struct LayoutEntry {
81 binding: u32,
82 visibility: u32,
83 slot: Slot,
84 dynamic: bool,
85 min_binding_size: u64,
86 filterable: bool,
88}
89
90pub struct Gpu {
91 pub device: wgpu::Device,
92 pub queue: wgpu::Queue,
93 pub format: wgpu::TextureFormat,
94}
95
96pub enum Target {
97 Window { surface: wgpu::Surface<'static>, config: wgpu::SurfaceConfiguration },
98 Offscreen { texture: wgpu::Texture },
99}
100
101struct Frame {
102 pass: wgpu::RenderPass<'static>,
103 encoder: wgpu::CommandEncoder,
104 surface_texture: Option<wgpu::SurfaceTexture>,
105 empty_viewport: bool,
107 empty_scissor: bool,
108}
109
110struct Blit {
111 pipeline: wgpu::RenderPipeline,
112 sampler: wgpu::Sampler,
113 texture: Option<(wgpu::Texture, wgpu::BindGroup, u32, u32)>,
114}
115
116pub struct Gfx {
117 gpu: Option<Gpu>,
118 target: Option<Target>,
119 width: u32,
120 height: u32,
121 objects: Vec<Obj>,
122 meta: Vec<Meta>,
123 msaa: Option<wgpu::TextureView>,
124 depth: Option<wgpu::TextureView>,
125 frame: Option<Frame>,
126 blit: Option<Blit>,
127 pub used: bool,
129}
130
131type R<T> = Result<T, String>;
132
133impl Gfx {
134 pub fn null() -> Gfx {
136 Gfx { gpu: None, target: None, width: 1280, height: 720, objects: Vec::new(), meta: Vec::new(), msaa: None, depth: None, frame: None, blit: None, used: false }
137 }
138
139 pub fn for_window(window: Arc<winit::window::Window>) -> R<Gfx> {
141 let size = window.inner_size();
142 let instance = wgpu::Instance::new(wgpu::InstanceDescriptor::new_without_display_handle());
143 let surface = instance.create_surface(window).map_err(|e| e.to_string())?;
144 let adapter = pollster::block_on(instance.request_adapter(&wgpu::RequestAdapterOptions {
145 power_preference: wgpu::PowerPreference::HighPerformance,
146 compatible_surface: Some(&surface),
147 ..Default::default()
148 }))
149 .map_err(|e| format!("no GPU adapter: {e}"))?;
150 let gpu = open_device(&adapter, |caps_formats| pick_format(caps_formats), &surface)?;
151 let mut config = surface
152 .get_default_config(&adapter, size.width.max(1), size.height.max(1))
153 .ok_or("surface not supported by adapter")?;
154 config.format = gpu.format;
155 config.present_mode = wgpu::PresentMode::AutoVsync;
156 surface.configure(&gpu.device, &config);
157 eprintln!("[gasm] gpu: {} ({:?}), surface {:?}", adapter.get_info().name, adapter.get_info().backend, gpu.format);
158 let mut g = Gfx::with_gpu(gpu, Target::Window { surface, config }, size.width.max(1), size.height.max(1));
159 g.recreate_attachments();
160 Ok(g)
161 }
162
163 pub fn offscreen(width: u32, height: u32) -> R<Gfx> {
165 let instance = wgpu::Instance::new(wgpu::InstanceDescriptor::new_without_display_handle());
166 let adapter = pollster::block_on(instance.request_adapter(&wgpu::RequestAdapterOptions::default()))
167 .map_err(|e| format!("no GPU adapter: {e}"))?;
168 let (device, queue) = pollster::block_on(adapter.request_device(&wgpu::DeviceDescriptor::default()))
169 .map_err(|e| e.to_string())?;
170 device.on_uncaptured_error(Arc::new(|e| eprintln!("[gasm] gpu error: {e}")));
171 let format = wgpu::TextureFormat::Rgba8Unorm;
172 let texture = device.create_texture(&wgpu::TextureDescriptor {
173 label: Some("offscreen"),
174 size: wgpu::Extent3d { width, height, depth_or_array_layers: 1 },
175 mip_level_count: 1,
176 sample_count: 1,
177 dimension: wgpu::TextureDimension::D2,
178 format,
179 usage: wgpu::TextureUsages::RENDER_ATTACHMENT | wgpu::TextureUsages::COPY_SRC,
180 view_formats: &[],
181 });
182 let mut g = Gfx::with_gpu(Gpu { device, queue, format }, Target::Offscreen { texture }, width, height);
183 g.recreate_attachments();
184 Ok(g)
185 }
186
187 fn with_gpu(gpu: Gpu, target: Target, width: u32, height: u32) -> Gfx {
188 let blit = Some(Blit::new(&gpu));
189 Gfx { gpu: Some(gpu), target: Some(target), width, height, objects: Vec::new(), meta: Vec::new(), msaa: None, depth: None, frame: None, blit, used: false }
190 }
191
192 pub fn size(&self) -> (u32, u32) {
193 (self.width, self.height)
194 }
195
196 pub fn resize(&mut self, width: u32, height: u32) {
197 if width == 0 || height == 0 || (width, height) == (self.width, self.height) {
198 return;
199 }
200 self.width = width;
201 self.height = height;
202 if let (Some(gpu), Some(Target::Window { surface, config })) = (&self.gpu, &mut self.target) {
203 config.width = width;
204 config.height = height;
205 surface.configure(&gpu.device, config);
206 }
207 self.recreate_attachments();
208 }
209
210 fn recreate_attachments(&mut self) {
211 let Some(gpu) = &self.gpu else { return };
212 let make = |format, label| {
213 gpu.device
214 .create_texture(&wgpu::TextureDescriptor {
215 label: Some(label),
216 size: wgpu::Extent3d { width: self.width, height: self.height, depth_or_array_layers: 1 },
217 mip_level_count: 1,
218 sample_count: SAMPLE_COUNT,
219 dimension: wgpu::TextureDimension::D2,
220 format,
221 usage: wgpu::TextureUsages::RENDER_ATTACHMENT,
222 view_formats: &[],
223 })
224 .create_view(&Default::default())
225 };
226 self.msaa = Some(make(gpu.format, "msaa"));
227 self.depth = Some(make(DEPTH_FORMAT, "depth"));
228 }
229
230 fn add(&mut self, obj: Obj, meta: Meta) -> u32 {
233 self.objects.push(obj);
234 self.meta.push(meta);
235 self.objects.len() as u32
236 }
237
238 fn meta(&self, h: u32) -> R<&Meta> {
239 self.meta.get((h as usize).wrapping_sub(1)).ok_or_else(|| format!("gfx: invalid handle {h}"))
240 }
241
242 fn expect(&self, h: u32, want: &str) -> R<&Meta> {
244 let m = self.meta(h)?;
245 if m.kind() != want {
246 return Err(format!("gfx: handle {h} is a {}, not a {want}", m.kind()));
247 }
248 Ok(m)
249 }
250
251 fn get(&self, h: u32) -> R<&Obj> {
252 self.objects.get((h as usize).wrapping_sub(1)).ok_or_else(|| format!("gfx: invalid handle {h}"))
253 }
254
255 fn shader(&self, h: u32) -> R<&wgpu::ShaderModule> {
256 match self.get(h)? { Obj::Shader(s) => Ok(s), _ => Err(format!("gfx: handle {h} is not a shader")) }
257 }
258 fn buffer(&self, h: u32) -> R<&wgpu::Buffer> {
259 match self.get(h)? { Obj::Buffer(b) => Ok(b), _ => Err(format!("gfx: handle {h} is not a buffer")) }
260 }
261 fn pipeline(&self, h: u32) -> R<&wgpu::RenderPipeline> {
262 match self.get(h)? { Obj::Pipeline(p) => Ok(p), _ => Err(format!("gfx: handle {h} is not a pipeline")) }
263 }
264 fn bind_group(&self, h: u32) -> R<&wgpu::BindGroup> {
265 match self.get(h)? { Obj::BindGroup(b) => Ok(b), _ => Err(format!("gfx: handle {h} is not a bind group")) }
266 }
267 fn layout(&self, h: u32) -> R<&wgpu::BindGroupLayout> {
268 match self.get(h)? { Obj::Layout(l) => Ok(l), _ => Err(format!("gfx: handle {h} is not a bind group layout")) }
269 }
270
271 fn scoped<T>(&self, what: &str, f: impl FnOnce(&Gpu) -> R<T>) -> R<T> {
273 let gpu = self.gpu.as_ref().unwrap();
274 let scope = gpu.device.push_error_scope(wgpu::ErrorFilter::Validation);
275 let out = f(gpu);
276 if let Some(e) = pollster::block_on(scope.pop()) {
277 return Err(format!("gfx: {what}: {e}"));
278 }
279 out
280 }
281
282 pub fn create_shader(&mut self, wgsl: &str) -> R<u32> {
285 if self.gpu.is_none() {
286 return Ok(self.add(Obj::Null, Meta::Shader));
287 }
288 let m = self.scoped("create_shader", |gpu| {
289 Ok(gpu.device.create_shader_module(wgpu::ShaderModuleDescriptor {
290 label: None,
291 source: wgpu::ShaderSource::Wgsl(wgsl.into()),
292 }))
293 })?;
294 Ok(self.add(Obj::Shader(m), Meta::Shader))
295 }
296
297 pub fn create_buffer(&mut self, size: u32, usage: u32) -> R<u32> {
298 if size == 0 || size % 4 != 0 {
299 return Err(format!("gfx.create_buffer: size {size} must be a non-zero multiple of 4"));
300 }
301 let meta = Meta::Buffer { size: size as u64, usage: usage | 0x08 };
302 let Some(gpu) = &self.gpu else { return Ok(self.add(Obj::Null, meta)) };
303 let usage = wgpu::BufferUsages::from_bits_truncate(usage) | wgpu::BufferUsages::COPY_DST;
305 let b = gpu.device.create_buffer(&wgpu::BufferDescriptor { label: None, size: size as u64, usage, mapped_at_creation: false });
306 Ok(self.add(Obj::Buffer(b), meta))
307 }
308
309 pub fn write_buffer(&mut self, h: u32, offset: u32, data: &[u8]) -> R<()> {
310 if offset % 4 != 0 || data.len() % 4 != 0 {
311 return Err(format!("gfx.write_buffer: offset {offset} and length {} must be multiples of 4", data.len()));
312 }
313 if self.gpu.is_none() {
314 return self.get(h).map(|_| ());
315 }
316 let buf = self.buffer(h)?;
317 if offset as u64 + data.len() as u64 > buf.size() {
318 return Err(format!("gfx.write_buffer: {}+{} exceeds buffer size {}", offset, data.len(), buf.size()));
319 }
320 self.gpu.as_ref().unwrap().queue.write_buffer(buf, offset as u64, data);
321 Ok(())
322 }
323
324 pub fn create_pipeline(&mut self, json: &str) -> R<u32> {
325 let d: Value = serde_json::from_str(json).map_err(|e| format!("gfx.create_pipeline: invalid JSON: {e}"))?;
326 let layouts = self.pipeline_layouts(&d)?;
327 if self.gpu.is_none() {
328 return Ok(self.add(Obj::Null, Meta::Pipeline));
329 }
330 let p = self.build_pipeline(&d, layouts)?;
331 Ok(self.add(Obj::Pipeline(p), Meta::Pipeline))
332 }
333
334 fn pipeline_layouts(&self, d: &Value) -> R<Option<Vec<u32>>> {
336 match d.get("layout") {
337 None => Ok(None),
338 Some(Value::String(s)) if s == "auto" => Ok(None),
339 Some(Value::Array(a)) => {
340 let mut out = Vec::new();
341 for v in a {
342 let h = v.as_u64().ok_or("pipeline: layout entries must be bind group layout handles")? as u32;
343 self.expect(h, "bind group layout")?;
344 out.push(h);
345 }
346 Ok(Some(out))
347 }
348 Some(o) => Err(format!("pipeline: layout must be \"auto\" or an array of bind group layout handles, got {o}")),
349 }
350 }
351
352 pub fn create_bind_group(&mut self, json: &str) -> R<u32> {
353 let d: Value = serde_json::from_str(json).map_err(|e| format!("gfx.create_bind_group: invalid JSON: {e}"))?;
354 let entries_json = d.get("entries").and_then(Value::as_array).cloned().unwrap_or_default();
355 let mut resolved = Vec::new(); for e in &entries_json {
358 let binding = uint(e, "binding")?;
359 if resolved.iter().any(|r: &(u32, Slot, u32, u64, Option<u64>)| r.0 == binding) {
360 return Err(format!("bind group: binding {binding} appears twice"));
361 }
362 if let Some(h) = e.get("buffer").and_then(Value::as_u64) {
363 let Meta::Buffer { size, .. } = self.expect(h as u32, "buffer")? else { unreachable!() };
364 let offset = e.get("offset").and_then(Value::as_u64).unwrap_or(0);
365 let bsize = e.get("size").and_then(Value::as_u64);
366 if offset + bsize.unwrap_or(0) > *size || offset > *size {
367 return Err(format!("bind group: binding {binding}: range {offset}+{} exceeds buffer size {size}", bsize.unwrap_or(0)));
368 }
369 resolved.push((binding, Slot::Uniform, h as u32, offset, bsize));
370 } else if let Some(h) = e.get("texture").and_then(Value::as_u64) {
371 self.expect(h as u32, "texture")?;
372 resolved.push((binding, Slot::Texture, h as u32, 0, None));
373 } else if let Some(h) = e.get("sampler").and_then(Value::as_u64) {
374 self.expect(h as u32, "sampler")?;
375 resolved.push((binding, Slot::Sampler, h as u32, 0, None));
376 } else {
377 return Err(format!("bind group: binding {binding} needs a \"buffer\", \"texture\" or \"sampler\""));
378 }
379 }
380 let mut dynamic = Vec::new();
381 let explicit = d.get("layout").and_then(Value::as_u64).map(|h| h as u32);
382 if let Some(lh) = explicit {
383 let Meta::Layout { entries } = self.expect(lh, "bind group layout")?.clone() else { unreachable!() };
384 if resolved.len() != entries.len() {
385 return Err(format!("bind group: layout {lh} has {} entries, got {}", entries.len(), resolved.len()));
386 }
387 for le in &entries {
388 let r = resolved.iter().find(|r| r.0 == le.binding).ok_or_else(|| format!("bind group: missing binding {}", le.binding))?;
389 let is_buffer = matches!(le.slot, Slot::Uniform | Slot::Storage);
390 if (is_buffer && r.1 != Slot::Uniform) || (!is_buffer && r.1 != le.slot) {
391 return Err(format!("bind group: binding {} has the wrong resource kind for its layout", le.binding));
392 }
393 if is_buffer {
394 let Meta::Buffer { size, usage } = self.meta(r.2)?.clone() else { unreachable!() };
395 let need = if le.slot == Slot::Uniform { 0x40 } else { 0x80 };
396 if usage & need == 0 {
397 return Err(format!("bind group: binding {}: buffer {} lacks {} usage", le.binding, r.2, if need == 0x40 { "UNIFORM" } else { "STORAGE" }));
398 }
399 let bsize = r.4.unwrap_or(size - r.3);
400 if bsize < le.min_binding_size {
401 return Err(format!("bind group: binding {}: size {bsize} is below minBindingSize {}", le.binding, le.min_binding_size));
402 }
403 if le.dynamic {
404 dynamic.push((le.binding, DynEntry { buffer_size: size, offset: r.3, size: bsize }));
405 }
406 }
407 }
408 dynamic.sort_by_key(|d| d.0);
409 } else {
410 self.expect(uint(&d, "pipeline").map_err(|_| "bind group: needs \"layout\" or \"pipeline\"".to_string())?, "pipeline")?;
411 }
412 let meta = Meta::BindGroup { dynamic: dynamic.into_iter().map(|d| d.1).collect() };
413 if self.gpu.is_none() {
414 return Ok(self.add(Obj::Null, meta));
415 }
416 let layout = match explicit {
417 Some(lh) => self.layout(lh)?.clone(),
418 None => self.pipeline(uint(&d, "pipeline")?)?.get_bind_group_layout(d.get("group").and_then(Value::as_u64).unwrap_or(0) as u32),
419 };
420 let mut entries = Vec::new();
421 for (binding, slot, h, offset, size) in &resolved {
422 let resource = match slot {
423 Slot::Texture => match self.get(*h)? { Obj::Texture(_, v) => wgpu::BindingResource::TextureView(v), _ => unreachable!() },
424 Slot::Sampler => match self.get(*h)? { Obj::Sampler(s) => wgpu::BindingResource::Sampler(s), _ => unreachable!() },
425 _ => wgpu::BindingResource::Buffer(wgpu::BufferBinding {
426 buffer: self.buffer(*h)?,
427 offset: *offset,
428 size: size.and_then(wgpu::BufferSize::new),
429 }),
430 };
431 entries.push(wgpu::BindGroupEntry { binding: *binding, resource });
432 }
433 let bg = self.scoped("create_bind_group", |gpu| {
434 Ok(gpu.device.create_bind_group(&wgpu::BindGroupDescriptor { label: None, layout: &layout, entries: &entries }))
435 })?;
436 Ok(self.add(Obj::BindGroup(bg), meta))
437 }
438
439 pub fn create_bind_group_layout(&mut self, json: &str) -> R<u32> {
440 let d: Value = serde_json::from_str(json).map_err(|e| format!("gfx.create_bind_group_layout: invalid JSON: {e}"))?;
441 let mut entries: Vec<LayoutEntry> = Vec::new();
442 for e in d.get("entries").and_then(Value::as_array).cloned().unwrap_or_default() {
443 let binding = uint(&e, "binding")?;
444 if entries.iter().any(|x| x.binding == binding) {
445 return Err(format!("bind group layout: binding {binding} appears twice"));
446 }
447 let visibility = uint(&e, "visibility")?;
448 if visibility == 0 || visibility & !3 != 0 {
449 return Err(format!("bind group layout: binding {binding}: visibility {visibility} must be GASM_STAGE_VERTEX (1) and/or GASM_STAGE_FRAGMENT (2)"));
450 }
451 let entry = if let Some(b) = e.get("buffer") {
452 let slot = match b.get("type").and_then(Value::as_str).unwrap_or("uniform") {
453 "uniform" => Slot::Uniform,
454 "read-only-storage" => Slot::Storage,
455 o => return Err(format!("bind group layout: binding {binding}: unsupported buffer type {o:?}")),
456 };
457 LayoutEntry {
458 binding, visibility, slot,
459 dynamic: b.get("hasDynamicOffset").and_then(Value::as_bool).unwrap_or(false),
460 min_binding_size: b.get("minBindingSize").and_then(Value::as_u64).unwrap_or(0),
461 filterable: false,
462 }
463 } else if let Some(t) = e.get("texture") {
464 let filterable = match t.get("sampleType").and_then(Value::as_str).unwrap_or("float") {
465 "float" => true,
466 "unfilterable-float" => false,
467 o => return Err(format!("bind group layout: binding {binding}: unsupported sampleType {o:?}")),
468 };
469 if let Some(v) = t.get("viewDimension").and_then(Value::as_str)
470 && v != "2d"
471 {
472 return Err(format!("bind group layout: binding {binding}: only viewDimension \"2d\" is supported, got {v:?}"));
473 }
474 if t.get("multisampled").and_then(Value::as_bool).unwrap_or(false) {
475 return Err(format!("bind group layout: binding {binding}: multisampled textures are not supported"));
476 }
477 LayoutEntry { binding, visibility, slot: Slot::Texture, dynamic: false, min_binding_size: 0, filterable }
478 } else if let Some(sm) = e.get("sampler") {
479 let filterable = match sm.get("type").and_then(Value::as_str).unwrap_or("filtering") {
480 "filtering" => true,
481 "non-filtering" => false,
482 o => return Err(format!("bind group layout: binding {binding}: unsupported sampler type {o:?}")),
483 };
484 LayoutEntry { binding, visibility, slot: Slot::Sampler, dynamic: false, min_binding_size: 0, filterable }
485 } else {
486 return Err(format!("bind group layout: binding {binding} needs \"buffer\", \"texture\" or \"sampler\""));
487 };
488 entries.push(entry);
489 }
490 entries.sort_by_key(|e| e.binding);
491 let meta = Meta::Layout { entries: entries.clone() };
492 if self.gpu.is_none() {
493 return Ok(self.add(Obj::Null, meta));
494 }
495 let wentries: Vec<wgpu::BindGroupLayoutEntry> = entries
496 .iter()
497 .map(|e| wgpu::BindGroupLayoutEntry {
498 binding: e.binding,
499 visibility: wgpu::ShaderStages::from_bits_truncate(e.visibility),
500 ty: match e.slot {
501 Slot::Uniform | Slot::Storage => wgpu::BindingType::Buffer {
502 ty: if e.slot == Slot::Uniform {
503 wgpu::BufferBindingType::Uniform
504 } else {
505 wgpu::BufferBindingType::Storage { read_only: true }
506 },
507 has_dynamic_offset: e.dynamic,
508 min_binding_size: wgpu::BufferSize::new(e.min_binding_size),
509 },
510 Slot::Texture => wgpu::BindingType::Texture {
511 sample_type: wgpu::TextureSampleType::Float { filterable: e.filterable },
512 view_dimension: wgpu::TextureViewDimension::D2,
513 multisampled: false,
514 },
515 Slot::Sampler => wgpu::BindingType::Sampler(if e.filterable {
516 wgpu::SamplerBindingType::Filtering
517 } else {
518 wgpu::SamplerBindingType::NonFiltering
519 }),
520 },
521 count: None,
522 })
523 .collect();
524 let l = self.scoped("create_bind_group_layout", |gpu| {
525 Ok(gpu.device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor { label: None, entries: &wentries }))
526 })?;
527 Ok(self.add(Obj::Layout(l), meta))
528 }
529
530 pub fn create_texture(&mut self, json: &str) -> R<u32> {
531 let d: Value = serde_json::from_str(json).map_err(|e| format!("gfx.create_texture: invalid JSON: {e}"))?;
532 let size = d.get("size").and_then(Value::as_array).ok_or("texture: missing \"size\": [width, height]")?;
533 let dim = |i: usize| size.get(i).and_then(Value::as_u64).unwrap_or(0);
534 let (w, h) = (dim(0), dim(1));
535 if size.len() != 2 || w == 0 || h == 0 || w > MAX_TEXTURE_SIZE as u64 || h > MAX_TEXTURE_SIZE as u64 {
536 return Err(format!("texture: size must be [width, height], each 1-{MAX_TEXTURE_SIZE}"));
537 }
538 let (w, h) = (w as u32, h as u32);
539 let format = match d.get("format").and_then(Value::as_str).unwrap_or("rgba8unorm") {
540 "rgba8unorm" => wgpu::TextureFormat::Rgba8Unorm,
541 "rgba8unorm-srgb" => wgpu::TextureFormat::Rgba8UnormSrgb,
542 o => return Err(format!("texture: unsupported format {o:?} (rgba8unorm, rgba8unorm-srgb)")),
543 };
544 let max_mips = 32 - w.max(h).leading_zeros();
545 let mips = d.get("mipLevelCount").and_then(Value::as_u64).unwrap_or(1);
546 if mips == 0 || mips > max_mips as u64 {
547 return Err(format!("texture: mipLevelCount {mips} must be 1-{max_mips} for {w}x{h}"));
548 }
549 let meta = Meta::Texture { width: w, height: h, mips: mips as u32 };
550 let Some(gpu) = &self.gpu else { return Ok(self.add(Obj::Null, meta)) };
551 let t = gpu.device.create_texture(&wgpu::TextureDescriptor {
552 label: None,
553 size: wgpu::Extent3d { width: w, height: h, depth_or_array_layers: 1 },
554 mip_level_count: mips as u32,
555 sample_count: 1,
556 dimension: wgpu::TextureDimension::D2,
557 format,
558 usage: wgpu::TextureUsages::TEXTURE_BINDING | wgpu::TextureUsages::COPY_DST,
559 view_formats: &[],
560 });
561 let view = t.create_view(&Default::default());
562 Ok(self.add(Obj::Texture(t, view), meta))
563 }
564
565 #[allow(clippy::too_many_arguments)]
566 pub fn write_texture(&mut self, h: u32, mip: u32, x: u32, y: u32, w: u32, ht: u32, data: &[u8]) -> R<()> {
567 let Meta::Texture { width, height, mips } = *self.expect(h, "texture")? else { unreachable!() };
568 if mip >= mips {
569 return Err(format!("gfx.write_texture: mip {mip} out of range (texture has {mips})"));
570 }
571 let (lw, lh) = ((width >> mip).max(1), (height >> mip).max(1));
572 if w == 0 || ht == 0 || x as u64 + w as u64 > lw as u64 || y as u64 + ht as u64 > lh as u64 {
573 return Err(format!("gfx.write_texture: region {x},{y} {w}x{ht} is outside mip {mip} ({lw}x{lh})"));
574 }
575 if data.len() as u64 != w as u64 * ht as u64 * 4 {
576 return Err(format!("gfx.write_texture: len {} must be width*height*4 = {}", data.len(), w as u64 * ht as u64 * 4));
577 }
578 let (Some(gpu), Obj::Texture(t, _)) = (&self.gpu, self.get(h)?) else { return Ok(()) };
579 gpu.queue.write_texture(
580 wgpu::TexelCopyTextureInfo { texture: t, mip_level: mip, origin: wgpu::Origin3d { x, y, z: 0 }, aspect: wgpu::TextureAspect::All },
581 data,
582 wgpu::TexelCopyBufferLayout { offset: 0, bytes_per_row: Some(w * 4), rows_per_image: Some(ht) },
583 wgpu::Extent3d { width: w, height: ht, depth_or_array_layers: 1 },
584 );
585 Ok(())
586 }
587
588 pub fn create_sampler(&mut self, json: &str) -> R<u32> {
589 let d: Value = serde_json::from_str(json).map_err(|e| format!("gfx.create_sampler: invalid JSON: {e}"))?;
590 let address = |k: &str| -> R<wgpu::AddressMode> {
591 Ok(match d.get(k).and_then(Value::as_str).unwrap_or("clamp-to-edge") {
592 "clamp-to-edge" => wgpu::AddressMode::ClampToEdge,
593 "repeat" => wgpu::AddressMode::Repeat,
594 "mirror-repeat" => wgpu::AddressMode::MirrorRepeat,
595 o => return Err(format!("sampler: unsupported {k} {o:?}")),
596 })
597 };
598 let linear = |k: &str| -> R<bool> {
599 match d.get(k).and_then(Value::as_str).unwrap_or("nearest") {
600 "nearest" => Ok(false),
601 "linear" => Ok(true),
602 o => Err(format!("sampler: unsupported {k} {o:?}")),
603 }
604 };
605 let (u, v) = (address("addressModeU")?, address("addressModeV")?);
606 let (mag, min, mip) = (linear("magFilter")?, linear("minFilter")?, linear("mipmapFilter")?);
607 let lod_min = d.get("lodMinClamp").and_then(Value::as_f64).unwrap_or(0.0) as f32;
608 let lod_max = d.get("lodMaxClamp").and_then(Value::as_f64).unwrap_or(32.0) as f32;
609 if !(lod_min >= 0.0 && lod_max >= lod_min) {
610 return Err(format!("sampler: lodMinClamp {lod_min} / lodMaxClamp {lod_max} must satisfy 0 <= min <= max"));
611 }
612 let aniso = d.get("maxAnisotropy").and_then(Value::as_u64).unwrap_or(1);
613 if !(1..=16).contains(&aniso) {
614 return Err(format!("sampler: maxAnisotropy {aniso} must be 1-16"));
615 }
616 if aniso > 1 && !(mag && min && mip) {
617 return Err("sampler: maxAnisotropy > 1 needs linear magFilter, minFilter and mipmapFilter".into());
618 }
619 let Some(gpu) = &self.gpu else { return Ok(self.add(Obj::Null, Meta::Sampler)) };
620 let f = |l: bool| if l { wgpu::FilterMode::Linear } else { wgpu::FilterMode::Nearest };
621 let smp = gpu.device.create_sampler(&wgpu::SamplerDescriptor {
622 label: None,
623 address_mode_u: u,
624 address_mode_v: v,
625 address_mode_w: wgpu::AddressMode::ClampToEdge,
626 mag_filter: f(mag),
627 min_filter: f(min),
628 mipmap_filter: if mip { wgpu::MipmapFilterMode::Linear } else { wgpu::MipmapFilterMode::Nearest },
629 lod_min_clamp: lod_min,
630 lod_max_clamp: lod_max,
631 compare: None,
632 anisotropy_clamp: aniso as u16,
633 border_color: None,
634 });
635 Ok(self.add(Obj::Sampler(smp), Meta::Sampler))
636 }
637
638 fn build_pipeline(&self, d: &Value, group_layouts: Option<Vec<u32>>) -> R<wgpu::RenderPipeline> {
639 let gpu = self.gpu.as_ref().unwrap();
640 let v = d.get("vertex").ok_or("pipeline: missing vertex")?;
641 let vmod = self.shader(uint(v, "module")?)?;
642 let buffers_json = v.get("buffers").and_then(Value::as_array).cloned().unwrap_or_default();
643 let mut attrs: Vec<Vec<wgpu::VertexAttribute>> = Vec::new();
644 for b in &buffers_json {
645 let mut list = Vec::new();
646 for a in b.get("attributes").and_then(Value::as_array).cloned().unwrap_or_default() {
647 list.push(wgpu::VertexAttribute {
648 format: vertex_format(string(&a, "format")?)?,
649 offset: a.get("offset").and_then(Value::as_u64).unwrap_or(0),
650 shader_location: uint(&a, "shaderLocation")?,
651 });
652 }
653 attrs.push(list);
654 }
655 let layouts: Vec<Option<wgpu::VertexBufferLayout>> = buffers_json
656 .iter()
657 .zip(&attrs)
658 .map(|(b, a)| {
659 Some(wgpu::VertexBufferLayout {
660 array_stride: b.get("arrayStride").and_then(Value::as_u64).unwrap_or(0),
661 step_mode: match b.get("stepMode").and_then(Value::as_str) {
662 Some("instance") => wgpu::VertexStepMode::Instance,
663 _ => wgpu::VertexStepMode::Vertex,
664 },
665 attributes: a,
666 })
667 })
668 .collect();
669
670 let mut targets = Vec::new();
671 let fragment_module;
672 let f = d.get("fragment");
673 if let Some(f) = f {
674 fragment_module = Some(self.shader(uint(f, "module")?)?);
675 for t in f.get("targets").and_then(Value::as_array).cloned().unwrap_or_default() {
676 let format = match t.get("format").and_then(Value::as_str) {
677 Some("surface") | None => gpu.format,
678 Some(other) => return Err(format!("pipeline: color target format must be \"surface\", got {other:?}")),
679 };
680 let blend = match t.get("blend") {
681 Some(b) => Some(wgpu::BlendState {
682 color: blend_component(b.get("color"))?,
683 alpha: blend_component(b.get("alpha"))?,
684 }),
685 None => None,
686 };
687 let write_mask = t
688 .get("writeMask")
689 .and_then(Value::as_u64)
690 .map(|m| wgpu::ColorWrites::from_bits_truncate(m as u32))
691 .unwrap_or(wgpu::ColorWrites::ALL);
692 targets.push(Some(wgpu::ColorTargetState { format, blend, write_mask }));
693 }
694 } else {
695 fragment_module = None;
696 }
697
698 let p = d.get("primitive");
699 let ps = |k: &str| p.and_then(|p| p.get(k)).and_then(Value::as_str);
700 let primitive = wgpu::PrimitiveState {
701 topology: match ps("topology").unwrap_or("triangle-list") {
702 "point-list" => wgpu::PrimitiveTopology::PointList,
703 "line-list" => wgpu::PrimitiveTopology::LineList,
704 "line-strip" => wgpu::PrimitiveTopology::LineStrip,
705 "triangle-list" => wgpu::PrimitiveTopology::TriangleList,
706 "triangle-strip" => wgpu::PrimitiveTopology::TriangleStrip,
707 o => return Err(format!("pipeline: unknown topology {o:?}")),
708 },
709 strip_index_format: match ps("stripIndexFormat") {
710 Some("uint16") => Some(wgpu::IndexFormat::Uint16),
711 Some("uint32") => Some(wgpu::IndexFormat::Uint32),
712 _ => None,
713 },
714 front_face: match ps("frontFace").unwrap_or("ccw") {
715 "cw" => wgpu::FrontFace::Cw,
716 _ => wgpu::FrontFace::Ccw,
717 },
718 cull_mode: match ps("cullMode").unwrap_or("none") {
719 "front" => Some(wgpu::Face::Front),
720 "back" => Some(wgpu::Face::Back),
721 _ => None,
722 },
723 ..Default::default()
724 };
725
726 let depth_stencil = match d.get("depthStencil") {
727 Some(ds) => {
728 if let Some(f) = ds.get("format").and_then(Value::as_str)
729 && f != "depth24plus"
730 {
731 return Err(format!("pipeline: depthStencil format must be \"depth24plus\", got {f:?}"));
732 }
733 Some(wgpu::DepthStencilState {
734 format: DEPTH_FORMAT,
735 depth_write_enabled: Some(ds.get("depthWriteEnabled").and_then(Value::as_bool).unwrap_or(false)),
736 depth_compare: Some(compare(ds.get("depthCompare").and_then(Value::as_str).unwrap_or("always"))?),
737 stencil: Default::default(),
738 bias: wgpu::DepthBiasState {
739 constant: ds.get("depthBias").and_then(Value::as_i64).unwrap_or(0) as i32,
740 slope_scale: ds.get("depthBiasSlopeScale").and_then(Value::as_f64).unwrap_or(0.0) as f32,
741 clamp: ds.get("depthBiasClamp").and_then(Value::as_f64).unwrap_or(0.0) as f32,
742 },
743 })
744 }
745 None => Some(wgpu::DepthStencilState {
748 format: DEPTH_FORMAT,
749 depth_write_enabled: Some(false),
750 depth_compare: Some(wgpu::CompareFunction::Always),
751 stencil: Default::default(),
752 bias: Default::default(),
753 }),
754 };
755
756 let entry = |s: &Value| s.get("entryPoint").and_then(Value::as_str).map(str::to_owned);
757 let (v_entry, f_entry) = (entry(v), f.and_then(entry));
758 let bgls = match &group_layouts {
759 Some(hs) => Some(hs.iter().map(|h| self.layout(*h)).collect::<R<Vec<_>>>()?),
760 None => None,
761 };
762 self.scoped("create_pipeline", |gpu| {
763 let layout = bgls.map(|ls| {
764 let refs: Vec<Option<&wgpu::BindGroupLayout>> = ls.into_iter().map(Some).collect();
765 gpu.device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor { label: None, bind_group_layouts: &refs, immediate_size: 0 })
766 });
767 Ok(gpu.device.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
768 label: None,
769 layout: layout.as_ref(), vertex: wgpu::VertexState {
771 module: vmod,
772 entry_point: v_entry.as_deref(),
773 compilation_options: Default::default(),
774 buffers: &layouts,
775 },
776 primitive,
777 depth_stencil,
778 multisample: wgpu::MultisampleState { count: SAMPLE_COUNT, ..Default::default() },
779 fragment: fragment_module.map(|m| wgpu::FragmentState {
780 module: m,
781 entry_point: f_entry.as_deref(),
782 compilation_options: Default::default(),
783 targets: &targets,
784 }),
785 multiview_mask: None,
786 cache: None,
787 }))
788 })
789 }
790
791 pub fn begin_frame(&mut self, clear: [f32; 4], show: bool) -> R<bool> {
795 if self.frame.is_some() {
796 return Err("gfx.begin_frame called twice without end_frame".into());
797 }
798 self.used = true;
799 let (Some(gpu), Some(target), true) = (&self.gpu, &self.target, show) else { return Ok(false) };
800 let (resolve_view, surface_texture) = match target {
801 Target::Window { surface, config } => {
802 let st = match surface.get_current_texture() {
803 wgpu::CurrentSurfaceTexture::Success(t) | wgpu::CurrentSurfaceTexture::Suboptimal(t) => t,
804 wgpu::CurrentSurfaceTexture::Outdated | wgpu::CurrentSurfaceTexture::Lost => {
805 surface.configure(&gpu.device, config);
806 return Ok(false);
807 }
808 _ => return Ok(false), };
810 (st.texture.create_view(&Default::default()), Some(st))
811 }
812 Target::Offscreen { texture } => (texture.create_view(&Default::default()), None),
813 };
814 let mut encoder = gpu.device.create_command_encoder(&Default::default());
815 let c = clear.map(|v| v as f64);
816 let pass = encoder
817 .begin_render_pass(&wgpu::RenderPassDescriptor {
818 label: Some("guest"),
819 color_attachments: &[Some(wgpu::RenderPassColorAttachment {
820 view: self.msaa.as_ref().unwrap(),
821 depth_slice: None,
822 resolve_target: Some(&resolve_view),
823 ops: wgpu::Operations {
824 load: wgpu::LoadOp::Clear(wgpu::Color { r: c[0], g: c[1], b: c[2], a: c[3] }),
825 store: wgpu::StoreOp::Discard,
826 },
827 })],
828 depth_stencil_attachment: Some(wgpu::RenderPassDepthStencilAttachment {
829 view: self.depth.as_ref().unwrap(),
830 depth_ops: Some(wgpu::Operations { load: wgpu::LoadOp::Clear(1.0), store: wgpu::StoreOp::Discard }),
831 stencil_ops: None,
832 }),
833 timestamp_writes: None,
834 occlusion_query_set: None,
835 multiview_mask: None,
836 })
837 .forget_lifetime();
838 self.frame = Some(Frame { pass, encoder, surface_texture, empty_viewport: false, empty_scissor: false });
839 Ok(true)
840 }
841
842 pub fn set_pipeline(&mut self, h: u32) -> R<()> {
843 if self.gpu.is_none() { return self.get(h).map(|_| ()) }
844 let p = self.pipeline(h)?.clone();
845 if let Some(f) = &mut self.frame { f.pass.set_pipeline(&p); }
846 Ok(())
847 }
848
849 pub fn set_bind_group(&mut self, index: u32, h: u32) -> R<()> {
850 if self.gpu.is_none() { return self.get(h).map(|_| ()) }
851 let bg = self.bind_group(h)?.clone();
852 if let Some(f) = &mut self.frame { f.pass.set_bind_group(index, &bg, &[]); }
853 Ok(())
854 }
855
856 pub fn set_bind_group_offsets(&mut self, index: u32, h: u32, offsets: &[u32]) -> R<()> {
857 let Meta::BindGroup { dynamic } = self.expect(h, "bind group")? else { unreachable!() };
858 if offsets.len() != dynamic.len() {
859 return Err(format!("gfx.set_bind_group_offsets: bind group {h} has {} dynamic entries, got {} offsets", dynamic.len(), offsets.len()));
860 }
861 for (o, d) in offsets.iter().zip(dynamic) {
862 if o % OFFSET_ALIGNMENT != 0 {
863 return Err(format!("gfx.set_bind_group_offsets: offset {o} is not a multiple of {OFFSET_ALIGNMENT}"));
864 }
865 if *o as u64 + d.offset + d.size > d.buffer_size {
866 return Err(format!("gfx.set_bind_group_offsets: offset {o} + binding {}+{} exceeds buffer size {}", d.offset, d.size, d.buffer_size));
867 }
868 }
869 if self.gpu.is_none() { return Ok(()) }
870 let bg = self.bind_group(h)?.clone();
871 if let Some(f) = &mut self.frame { f.pass.set_bind_group(index, &bg, offsets); }
872 Ok(())
873 }
874
875 fn clamp_rect(&self, x: f64, y: f64, w: f64, h: f64) -> (f64, f64, f64, f64) {
877 let (dw, dh) = (self.width as f64, self.height as f64);
878 let (x0, y0) = (x.clamp(0.0, dw), y.clamp(0.0, dh));
879 let (x1, y1) = ((x + w).clamp(0.0, dw), (y + h).clamp(0.0, dh));
880 (x0, y0, x1 - x0, y1 - y0)
881 }
882
883 pub fn set_viewport(&mut self, x: f32, y: f32, w: f32, h: f32, min_depth: f32, max_depth: f32) -> R<()> {
884 if ![x, y, w, h, min_depth, max_depth].iter().all(|v| v.is_finite()) || w < 0.0 || h < 0.0 {
885 return Err(format!("gfx.set_viewport: invalid rectangle {x},{y} {w}x{h}"));
886 }
887 if !(0.0..=1.0).contains(&min_depth) || !(0.0..=1.0).contains(&max_depth) || min_depth > max_depth {
888 return Err(format!("gfx.set_viewport: depth range {min_depth}..{max_depth} must be within 0..1"));
889 }
890 let (x, y, w, h) = self.clamp_rect(x as f64, y as f64, w as f64, h as f64);
891 if let Some(f) = &mut self.frame {
892 f.empty_viewport = w <= 0.0 || h <= 0.0;
893 if !f.empty_viewport {
894 f.pass.set_viewport(x as f32, y as f32, w as f32, h as f32, min_depth, max_depth);
895 }
896 }
897 Ok(())
898 }
899
900 pub fn set_scissor_rect(&mut self, x: u32, y: u32, w: u32, h: u32) -> R<()> {
901 let (x, y, w, h) = self.clamp_rect(x as f64, y as f64, w as f64, h as f64);
902 if let Some(f) = &mut self.frame {
903 f.empty_scissor = w <= 0.0 || h <= 0.0;
904 if !f.empty_scissor {
905 f.pass.set_scissor_rect(x as u32, y as u32, w as u32, h as u32);
906 }
907 }
908 Ok(())
909 }
910
911 pub fn set_vertex_buffer(&mut self, slot: u32, h: u32, offset: u32) -> R<()> {
912 if self.gpu.is_none() { return self.get(h).map(|_| ()) }
913 let b = self.buffer(h)?.clone();
914 if let Some(f) = &mut self.frame { f.pass.set_vertex_buffer(slot, b.slice(offset as u64..)); }
915 Ok(())
916 }
917
918 pub fn set_index_buffer(&mut self, h: u32, format: u32, offset: u32) -> R<()> {
919 if self.gpu.is_none() { return self.get(h).map(|_| ()) }
920 let b = self.buffer(h)?.clone();
921 let fmt = if format == 1 { wgpu::IndexFormat::Uint32 } else { wgpu::IndexFormat::Uint16 };
922 if let Some(f) = &mut self.frame { f.pass.set_index_buffer(b.slice(offset as u64..), fmt); }
923 Ok(())
924 }
925
926 pub fn draw(&mut self, vc: u32, ic: u32, fv: u32, fi: u32) {
927 if let Some(f) = &mut self.frame
928 && !f.empty_viewport
929 && !f.empty_scissor
930 {
931 f.pass.draw(fv..fv + vc, fi..fi + ic);
932 }
933 }
934
935 pub fn draw_indexed(&mut self, ic: u32, inst: u32, first: u32, base: i32, fi: u32) {
936 if let Some(f) = &mut self.frame
937 && !f.empty_viewport
938 && !f.empty_scissor
939 {
940 f.pass.draw_indexed(first..first + ic, base, fi..fi + inst);
941 }
942 }
943
944 pub fn end_frame(&mut self) -> R<()> {
945 let Some(Frame { pass, encoder, surface_texture, .. }) = self.frame.take() else { return Ok(()) };
946 drop(pass);
947 let gpu = self.gpu.as_ref().unwrap();
948 gpu.queue.submit([encoder.finish()]);
949 if let Some(st) = surface_texture {
950 gpu.queue.present(st);
951 }
952 Ok(())
953 }
954
955 pub fn present_video(&mut self, rgba: &[u8], w: u32, h: u32) {
959 let (Some(gpu), Some(Target::Window { surface, config }), Some(blit)) = (&self.gpu, &self.target, &mut self.blit) else { return };
960 let st = match surface.get_current_texture() {
961 wgpu::CurrentSurfaceTexture::Success(t) | wgpu::CurrentSurfaceTexture::Suboptimal(t) => t,
962 wgpu::CurrentSurfaceTexture::Outdated | wgpu::CurrentSurfaceTexture::Lost => {
963 surface.configure(&gpu.device, config);
964 return;
965 }
966 _ => return,
967 };
968 blit.upload(gpu, rgba, w, h);
969 let view = st.texture.create_view(&Default::default());
970 let mut encoder = gpu.device.create_command_encoder(&Default::default());
971 {
972 let mut pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
973 label: Some("blit"),
974 color_attachments: &[Some(wgpu::RenderPassColorAttachment {
975 view: &view,
976 depth_slice: None,
977 resolve_target: None,
978 ops: wgpu::Operations { load: wgpu::LoadOp::Clear(wgpu::Color::BLACK), store: wgpu::StoreOp::Store },
979 })],
980 ..Default::default()
981 });
982 let (sw, sh) = (self.width as f32, self.height as f32);
984 let scale = (sw / w as f32).min(sh / h as f32);
985 let (vw, vh) = (w as f32 * scale, h as f32 * scale);
986 pass.set_viewport((sw - vw) / 2.0, (sh - vh) / 2.0, vw, vh, 0.0, 1.0);
987 pass.set_pipeline(&blit.pipeline);
988 pass.set_bind_group(0, &blit.texture.as_ref().unwrap().1, &[]);
989 pass.draw(0..3, 0..1);
990 }
991 gpu.queue.submit([encoder.finish()]);
992 gpu.queue.present(st);
993 }
994
995 pub fn read_offscreen(&self) -> Option<(u32, u32, Vec<u8>)> {
997 let (Some(gpu), Some(Target::Offscreen { texture })) = (&self.gpu, &self.target) else { return None };
998 let (w, h) = (self.width, self.height);
999 let row = (w * 4).div_ceil(256) * 256;
1000 let buf = gpu.device.create_buffer(&wgpu::BufferDescriptor {
1001 label: Some("readback"),
1002 size: (row * h) as u64,
1003 usage: wgpu::BufferUsages::COPY_DST | wgpu::BufferUsages::MAP_READ,
1004 mapped_at_creation: false,
1005 });
1006 let mut enc = gpu.device.create_command_encoder(&Default::default());
1007 enc.copy_texture_to_buffer(
1008 texture.as_image_copy(),
1009 wgpu::TexelCopyBufferInfo {
1010 buffer: &buf,
1011 layout: wgpu::TexelCopyBufferLayout { offset: 0, bytes_per_row: Some(row), rows_per_image: Some(h) },
1012 },
1013 wgpu::Extent3d { width: w, height: h, depth_or_array_layers: 1 },
1014 );
1015 gpu.queue.submit([enc.finish()]);
1016 buf.slice(..).map_async(wgpu::MapMode::Read, |_| {});
1017 gpu.device.poll(wgpu::PollType::wait_indefinitely()).ok()?;
1018 let data = buf.slice(..).get_mapped_range().ok()?;
1019 let mut out = Vec::with_capacity((w * h * 4) as usize);
1020 for y in 0..h as usize {
1021 out.extend_from_slice(&data[y * row as usize..y * row as usize + (w * 4) as usize]);
1022 }
1023 Some((w, h, out))
1024 }
1025}
1026
1027fn open_device(
1028 adapter: &wgpu::Adapter,
1029 choose: impl FnOnce(&[wgpu::TextureFormat]) -> wgpu::TextureFormat,
1030 surface: &wgpu::Surface,
1031) -> R<Gpu> {
1032 let (device, queue) =
1033 pollster::block_on(adapter.request_device(&wgpu::DeviceDescriptor::default())).map_err(|e| e.to_string())?;
1034 device.on_uncaptured_error(Arc::new(|e| eprintln!("[gasm] gpu error: {e}")));
1035 let caps = surface.get_capabilities(adapter);
1036 let format = choose(&caps.formats);
1037 Ok(Gpu { device, queue, format })
1038}
1039
1040fn pick_format(formats: &[wgpu::TextureFormat]) -> wgpu::TextureFormat {
1043 use wgpu::TextureFormat as F;
1044 [F::Bgra8Unorm, F::Rgba8Unorm].into_iter().find(|f| formats.contains(f)).unwrap_or(formats[0])
1045}
1046
1047fn uint(v: &Value, key: &str) -> R<u32> {
1048 v.get(key).and_then(Value::as_u64).map(|n| n as u32).ok_or_else(|| format!("descriptor: missing number {key:?}"))
1049}
1050
1051fn string<'a>(v: &'a Value, key: &str) -> R<&'a str> {
1052 v.get(key).and_then(Value::as_str).ok_or_else(|| format!("descriptor: missing string {key:?}"))
1053}
1054
1055fn vertex_format(s: &str) -> R<wgpu::VertexFormat> {
1056 use wgpu::VertexFormat as V;
1057 Ok(match s {
1058 "float32" => V::Float32,
1059 "float32x2" => V::Float32x2,
1060 "float32x3" => V::Float32x3,
1061 "float32x4" => V::Float32x4,
1062 "uint32" => V::Uint32,
1063 "uint32x2" => V::Uint32x2,
1064 "uint32x3" => V::Uint32x3,
1065 "uint32x4" => V::Uint32x4,
1066 "sint32" => V::Sint32,
1067 "sint32x2" => V::Sint32x2,
1068 "sint32x3" => V::Sint32x3,
1069 "sint32x4" => V::Sint32x4,
1070 "uint8x4" => V::Uint8x4,
1071 "unorm8x4" => V::Unorm8x4,
1072 "uint16x2" => V::Uint16x2,
1073 "uint16x4" => V::Uint16x4,
1074 "float16x2" => V::Float16x2,
1075 "float16x4" => V::Float16x4,
1076 o => return Err(format!("unsupported vertex format {o:?}")),
1077 })
1078}
1079
1080fn blend_component(v: Option<&Value>) -> R<wgpu::BlendComponent> {
1081 let Some(v) = v else { return Ok(wgpu::BlendComponent::REPLACE) };
1082 let factor = |k: &str, default: wgpu::BlendFactor| -> R<wgpu::BlendFactor> {
1083 use wgpu::BlendFactor as B;
1084 Ok(match v.get(k).and_then(Value::as_str) {
1085 None => default,
1086 Some("zero") => B::Zero,
1087 Some("one") => B::One,
1088 Some("src") => B::Src,
1089 Some("one-minus-src") => B::OneMinusSrc,
1090 Some("src-alpha") => B::SrcAlpha,
1091 Some("one-minus-src-alpha") => B::OneMinusSrcAlpha,
1092 Some("dst") => B::Dst,
1093 Some("one-minus-dst") => B::OneMinusDst,
1094 Some("dst-alpha") => B::DstAlpha,
1095 Some("one-minus-dst-alpha") => B::OneMinusDstAlpha,
1096 Some("src-alpha-saturated") => B::SrcAlphaSaturated,
1097 Some("constant") => B::Constant,
1098 Some("one-minus-constant") => B::OneMinusConstant,
1099 Some(o) => return Err(format!("unsupported blend factor {o:?}")),
1100 })
1101 };
1102 use wgpu::BlendOperation as O;
1103 Ok(wgpu::BlendComponent {
1104 src_factor: factor("srcFactor", wgpu::BlendFactor::One)?,
1105 dst_factor: factor("dstFactor", wgpu::BlendFactor::Zero)?,
1106 operation: match v.get("operation").and_then(Value::as_str).unwrap_or("add") {
1107 "add" => O::Add,
1108 "subtract" => O::Subtract,
1109 "reverse-subtract" => O::ReverseSubtract,
1110 "min" => O::Min,
1111 "max" => O::Max,
1112 o => return Err(format!("unsupported blend operation {o:?}")),
1113 },
1114 })
1115}
1116
1117fn compare(s: &str) -> R<wgpu::CompareFunction> {
1118 use wgpu::CompareFunction as C;
1119 Ok(match s {
1120 "never" => C::Never,
1121 "less" => C::Less,
1122 "equal" => C::Equal,
1123 "less-equal" => C::LessEqual,
1124 "greater" => C::Greater,
1125 "not-equal" => C::NotEqual,
1126 "greater-equal" => C::GreaterEqual,
1127 "always" => C::Always,
1128 o => return Err(format!("unsupported compare function {o:?}")),
1129 })
1130}
1131
1132impl Blit {
1133 const SHADER: &str = r#"
1134@group(0) @binding(0) var t: texture_2d<f32>;
1135@group(0) @binding(1) var s: sampler;
1136struct VO { @builtin(position) pos: vec4<f32>, @location(0) uv: vec2<f32> };
1137@vertex fn vs(@builtin(vertex_index) i: u32) -> VO {
1138 let uv = vec2<f32>(f32((i << 1u) & 2u), f32(i & 2u));
1139 var o: VO;
1140 o.pos = vec4<f32>(uv.x * 2.0 - 1.0, 1.0 - uv.y * 2.0, 0.0, 1.0);
1141 o.uv = uv;
1142 return o;
1143}
1144@fragment fn fs(i: VO) -> @location(0) vec4<f32> { return textureSample(t, s, i.uv); }
1145"#;
1146
1147 fn new(gpu: &Gpu) -> Blit {
1148 let module = gpu.device.create_shader_module(wgpu::ShaderModuleDescriptor {
1149 label: Some("blit"),
1150 source: wgpu::ShaderSource::Wgsl(Self::SHADER.into()),
1151 });
1152 let pipeline = gpu.device.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
1153 label: Some("blit"),
1154 layout: None,
1155 vertex: wgpu::VertexState { module: &module, entry_point: Some("vs"), compilation_options: Default::default(), buffers: &[] },
1156 primitive: Default::default(),
1157 depth_stencil: None,
1158 multisample: Default::default(),
1159 fragment: Some(wgpu::FragmentState {
1160 module: &module,
1161 entry_point: Some("fs"),
1162 compilation_options: Default::default(),
1163 targets: &[Some(gpu.format.into())],
1164 }),
1165 multiview_mask: None,
1166 cache: None,
1167 });
1168 let sampler = gpu.device.create_sampler(&wgpu::SamplerDescriptor {
1169 mag_filter: wgpu::FilterMode::Nearest,
1170 min_filter: wgpu::FilterMode::Nearest,
1171 ..Default::default()
1172 });
1173 Blit { pipeline, sampler, texture: None }
1174 }
1175
1176 fn upload(&mut self, gpu: &Gpu, rgba: &[u8], w: u32, h: u32) {
1177 if !matches!(&self.texture, Some((_, _, tw, th)) if (*tw, *th) == (w, h)) {
1178 let tex = gpu.device.create_texture_with_data(
1179 &gpu.queue,
1180 &wgpu::TextureDescriptor {
1181 label: Some("video"),
1182 size: wgpu::Extent3d { width: w, height: h, depth_or_array_layers: 1 },
1183 mip_level_count: 1,
1184 sample_count: 1,
1185 dimension: wgpu::TextureDimension::D2,
1186 format: wgpu::TextureFormat::Rgba8Unorm,
1187 usage: wgpu::TextureUsages::TEXTURE_BINDING | wgpu::TextureUsages::COPY_DST,
1188 view_formats: &[],
1189 },
1190 Default::default(),
1191 rgba,
1192 );
1193 let bg = gpu.device.create_bind_group(&wgpu::BindGroupDescriptor {
1194 label: None,
1195 layout: &self.pipeline.get_bind_group_layout(0),
1196 entries: &[
1197 wgpu::BindGroupEntry { binding: 0, resource: wgpu::BindingResource::TextureView(&tex.create_view(&Default::default())) },
1198 wgpu::BindGroupEntry { binding: 1, resource: wgpu::BindingResource::Sampler(&self.sampler) },
1199 ],
1200 });
1201 self.texture = Some((tex, bg, w, h));
1202 return;
1203 }
1204 let tex = &self.texture.as_ref().unwrap().0;
1205 gpu.queue.write_texture(
1206 tex.as_image_copy(),
1207 rgba,
1208 wgpu::TexelCopyBufferLayout { offset: 0, bytes_per_row: Some(w * 4), rows_per_image: Some(h) },
1209 wgpu::Extent3d { width: w, height: h, depth_or_array_layers: 1 },
1210 );
1211 }
1212}