1use std::cell::RefCell;
2use std::collections::HashMap;
3use std::sync::mpsc;
4use std::time::Instant;
5
6use super::compute::QualiaCompute;
7use super::memory::{
8 BindingUsage, BufferView, MemoryTopology, QualiaSlabAllocator, DEFAULT_BINDING_ALIGNMENT,
9};
10use super::oracle_ctx::OracleContext;
11#[cfg(not(target_arch = "wasm32"))]
12use crate::gpu_context::GpuAdapterCaps;
13use crate::wgsl_forge::{
14 emit_shader, AdapterConstraints, AdapterIdentity, ForgeError, HardwareProfile, KernelSpec,
15 Schedule, TargetBackend,
16};
17
18#[cfg(not(target_arch = "wasm32"))]
24fn create_instance() -> wgpu::Instance {
25 let mut desc = wgpu::InstanceDescriptor::new_without_display_handle();
26 if let Some(backends) = crate::gpu_context::qualia_backend_override() {
27 desc.backends = backends;
28 } else if cfg!(target_os = "windows") {
29 desc.backends = wgpu::Backends::DX12;
30 }
31 wgpu::Instance::new(desc)
32}
33
34#[cfg(not(target_arch = "wasm32"))]
40fn create_instance_for_coopmat() -> wgpu::Instance {
41 let mut desc = wgpu::InstanceDescriptor::new_without_display_handle();
43 if let Some(backends) = crate::gpu_context::qualia_backend_override() {
44 desc.backends = backends;
45 } else {
46 desc.backends = wgpu::Backends::VULKAN;
49 }
50 wgpu::Instance::new(desc)
51}
52
53#[cfg(not(target_arch = "wasm32"))]
59fn find_coopmat_adapter(instance: &wgpu::Instance) -> Option<wgpu::Adapter> {
60 let backends = if let Some(b) = crate::gpu_context::qualia_backend_override() {
61 b
62 } else {
63 wgpu::Backends::all()
64 };
65 let adapters = pollster::block_on(instance.enumerate_adapters(backends));
66 for adapter in &adapters {
68 let info = adapter.get_info();
69 if info.device_type != wgpu::DeviceType::DiscreteGpu {
70 continue;
71 }
72 let features = adapter.features();
73 if features.contains(wgpu::Features::EXPERIMENTAL_COOPERATIVE_MATRIX) {
74 return Some(adapter.clone());
75 }
76 }
77 for adapter in &adapters {
79 let features = adapter.features();
80 if features.contains(wgpu::Features::EXPERIMENTAL_COOPERATIVE_MATRIX) {
81 return Some(adapter.clone());
82 }
83 }
84 None
85}
86
87pub struct WgpuComputeContext {
88 pub device: wgpu::Device,
89 pub queue: wgpu::Queue,
90 pub adapter: AdapterIdentity,
91 pub constraints: AdapterConstraints,
92 pub profile: HardwareProfile,
94 pub allocator: QualiaSlabAllocator,
95 pub slab: wgpu::Buffer,
98 pub out_slab: wgpu::Buffer,
102 pub weight_slab: wgpu::Buffer,
107 weight_cursor: usize,
110 pub timestamp_supported: bool,
111 pub timestamp_period_ns: f32,
112 timestamp_resources: Option<TimestampResources>,
113 pipeline_cache: RefCell<HashMap<String, wgpu::ComputePipeline>>,
123}
124
125impl WgpuComputeContext {
126 pub fn new(capacity_bytes: usize) -> Result<Self, ForgeError> {
127 let instance = create_instance();
130 let adapter = pollster::block_on(instance.request_adapter(&wgpu::RequestAdapterOptions {
131 power_preference: wgpu::PowerPreference::HighPerformance,
132 ..Default::default()
133 }))
134 .map_err(|error| ForgeError::GpuUnavailable(error.to_string()))?;
135 let info = adapter.get_info();
136 let available_features = adapter.features();
137 let mut wanted =
140 wgpu::Features::TIMESTAMP_QUERY | wgpu::Features::SUBGROUP | wgpu::Features::SHADER_F16;
141 if crate::gpu_context::experimental_features_allowed() {
142 wanted |= wgpu::Features::EXPERIMENTAL_COOPERATIVE_MATRIX
143 | wgpu::Features::EXPERIMENTAL_RAY_QUERY;
144 }
145 let required_features = available_features & wanted;
146 let timestamp_supported = required_features.contains(wgpu::Features::TIMESTAMP_QUERY);
147 let limits = adapter.limits();
148 let mut constraints = AdapterConstraints::from_wgpu_limits(&limits);
151 constraints.supports_subgroups = available_features.contains(wgpu::Features::SUBGROUP);
152 constraints.supports_coopmat =
153 available_features.contains(wgpu::Features::EXPERIMENTAL_COOPERATIVE_MATRIX);
154 constraints.supports_rt_cores =
155 available_features.contains(wgpu::Features::EXPERIMENTAL_RAY_QUERY);
156 constraints.warp_size = if info.vendor == 0x1002 { 64 } else { 32 };
158 let required_limits = wgpu::Limits {
162 max_blas_primitive_count: limits.max_blas_primitive_count,
163 max_blas_geometry_count: limits.max_blas_geometry_count,
164 max_tlas_instance_count: limits.max_tlas_instance_count,
165 max_acceleration_structures_per_shader_stage: limits
166 .max_acceleration_structures_per_shader_stage,
167 ..wgpu::Limits::default()
168 };
169 let (device, queue) = pollster::block_on(adapter.request_device(&wgpu::DeviceDescriptor {
170 required_features,
171 required_limits,
172 experimental_features: if required_features.intersects(
177 wgpu::Features::EXPERIMENTAL_COOPERATIVE_MATRIX
178 | wgpu::Features::EXPERIMENTAL_RAY_QUERY,
179 ) {
180 unsafe { wgpu::ExperimentalFeatures::enabled() }
181 } else {
182 wgpu::ExperimentalFeatures::disabled()
183 },
184 ..Default::default()
185 }))
186 .map_err(|error| ForgeError::GpuUnavailable(error.to_string()))?;
187 let timestamp_period_ns = if timestamp_supported {
188 queue.get_timestamp_period()
189 } else {
190 0.0
191 };
192
193 let topology = if info.device_type == wgpu::DeviceType::IntegratedGpu
195 || info.device_type == wgpu::DeviceType::Cpu
196 {
197 MemoryTopology::Unified { zero_copy: true }
198 } else {
199 MemoryTopology::Discrete {
200 staging_required: true,
201 }
202 };
203 let memory_class = match topology {
204 MemoryTopology::Unified { .. } => "unified",
205 MemoryTopology::Discrete { .. } => "discrete",
206 }
207 .to_string();
208
209 let adapter = AdapterIdentity {
210 name: info.name,
211 vendor: info.vendor,
212 device: info.device,
213 device_type: format!("{:?}", info.device_type),
214 backend: format!("{:?}", info.backend),
215 driver: info.driver,
216 driver_info: info.driver_info,
217 };
218 let profile = HardwareProfile {
219 adapter: adapter.clone(),
220 constraints,
221 memory_class,
222 supports_timestamp_query: timestamp_supported,
223 max_compute_workgroup_storage_size: limits.max_compute_workgroup_storage_size,
224 max_storage_buffer_binding_size: limits.max_storage_buffer_binding_size,
225 min_storage_buffer_offset_alignment: limits.min_storage_buffer_offset_alignment,
226 min_uniform_buffer_offset_alignment: limits.min_uniform_buffer_offset_alignment,
227 };
228
229 let allocator = QualiaSlabAllocator::new(topology, capacity_bytes);
230
231 let slab = device.create_buffer(&wgpu::BufferDescriptor {
232 label: Some("forge-slab"),
233 size: capacity_bytes as u64,
234 usage: wgpu::BufferUsages::STORAGE
235 | wgpu::BufferUsages::UNIFORM
236 | wgpu::BufferUsages::COPY_DST
237 | wgpu::BufferUsages::COPY_SRC,
238 mapped_at_creation: false,
239 });
240
241 let out_slab = device.create_buffer(&wgpu::BufferDescriptor {
242 label: Some("forge-out-slab"),
243 size: capacity_bytes as u64,
244 usage: wgpu::BufferUsages::STORAGE
245 | wgpu::BufferUsages::COPY_DST
246 | wgpu::BufferUsages::COPY_SRC,
247 mapped_at_creation: false,
248 });
249
250 let weight_slab = device.create_buffer(&wgpu::BufferDescriptor {
251 label: Some("forge-weight-slab"),
252 size: capacity_bytes as u64,
253 usage: wgpu::BufferUsages::STORAGE
254 | wgpu::BufferUsages::COPY_DST
255 | wgpu::BufferUsages::COPY_SRC,
256 mapped_at_creation: false,
257 });
258 let timestamp_resources = timestamp_supported.then(|| TimestampResources::new(&device));
259
260 Ok(Self {
261 device,
262 queue,
263 adapter,
264 constraints,
265 profile,
266 allocator,
267 slab,
268 out_slab,
269 weight_slab,
270 weight_cursor: 0,
271 timestamp_supported,
272 timestamp_period_ns,
273 timestamp_resources,
274 pipeline_cache: RefCell::new(HashMap::new()),
275 })
276 }
277
278 #[cfg(not(target_arch = "wasm32"))]
291 pub fn new_for_coopmat(capacity_bytes: usize) -> Result<Self, ForgeError> {
292 let vk_instance = create_instance_for_coopmat();
294
295 if let Some(adapter) = find_coopmat_adapter(&vk_instance) {
297 let info = adapter.get_info();
298 log::info!(
299 "forge|coopmat_adapter|{}|backend={:?}|vendor=0x{:04x}:0x{:04x}",
300 info.name,
301 info.backend,
302 info.vendor,
303 info.device
304 );
305 let available_features = adapter.features();
306 let mut wanted = wgpu::Features::TIMESTAMP_QUERY
307 | wgpu::Features::SUBGROUP
308 | wgpu::Features::SHADER_F16;
309 if crate::gpu_context::experimental_features_allowed() {
310 wanted |= wgpu::Features::EXPERIMENTAL_COOPERATIVE_MATRIX
311 | wgpu::Features::EXPERIMENTAL_RAY_QUERY;
312 }
313 let required_features = available_features & wanted;
314 let timestamp_supported = required_features.contains(wgpu::Features::TIMESTAMP_QUERY);
315 let limits = adapter.limits();
316 let mut constraints = AdapterConstraints::from_wgpu_limits(&limits);
317 constraints.supports_subgroups = available_features.contains(wgpu::Features::SUBGROUP);
318 constraints.supports_coopmat =
319 available_features.contains(wgpu::Features::EXPERIMENTAL_COOPERATIVE_MATRIX);
320 constraints.supports_rt_cores =
321 available_features.contains(wgpu::Features::EXPERIMENTAL_RAY_QUERY);
322 constraints.warp_size = if info.vendor == 0x1002 { 64 } else { 32 };
323 let required_limits = wgpu::Limits {
324 max_blas_primitive_count: limits.max_blas_primitive_count,
325 max_blas_geometry_count: limits.max_blas_geometry_count,
326 max_tlas_instance_count: limits.max_tlas_instance_count,
327 max_acceleration_structures_per_shader_stage: limits
328 .max_acceleration_structures_per_shader_stage,
329 ..wgpu::Limits::default()
330 };
331 let (device, queue) =
332 pollster::block_on(adapter.request_device(&wgpu::DeviceDescriptor {
333 required_features,
334 required_limits,
335 experimental_features: if required_features.intersects(
336 wgpu::Features::EXPERIMENTAL_COOPERATIVE_MATRIX
337 | wgpu::Features::EXPERIMENTAL_RAY_QUERY,
338 ) {
339 unsafe { wgpu::ExperimentalFeatures::enabled() }
340 } else {
341 wgpu::ExperimentalFeatures::disabled()
342 },
343 ..Default::default()
344 }))
345 .map_err(|error| ForgeError::GpuUnavailable(error.to_string()))?;
346 let timestamp_period_ns = if timestamp_supported {
347 queue.get_timestamp_period()
348 } else {
349 0.0
350 };
351 let topology = if info.device_type == wgpu::DeviceType::IntegratedGpu
352 || info.device_type == wgpu::DeviceType::Cpu
353 {
354 MemoryTopology::Unified { zero_copy: true }
355 } else {
356 MemoryTopology::Discrete {
357 staging_required: true,
358 }
359 };
360 let memory_class = match topology {
361 MemoryTopology::Unified { .. } => "unified",
362 MemoryTopology::Discrete { .. } => "discrete",
363 }
364 .to_string();
365 let adapter_id = AdapterIdentity {
366 name: info.name,
367 vendor: info.vendor,
368 device: info.device,
369 device_type: format!("{:?}", info.device_type),
370 backend: format!("{:?}", info.backend),
371 driver: info.driver,
372 driver_info: info.driver_info,
373 };
374 let profile = HardwareProfile {
375 adapter: adapter_id.clone(),
376 constraints,
377 memory_class,
378 supports_timestamp_query: timestamp_supported,
379 max_compute_workgroup_storage_size: limits.max_compute_workgroup_storage_size,
380 max_storage_buffer_binding_size: limits.max_storage_buffer_binding_size,
381 min_storage_buffer_offset_alignment: limits.min_storage_buffer_offset_alignment,
382 min_uniform_buffer_offset_alignment: limits.min_uniform_buffer_offset_alignment,
383 };
384 let allocator = QualiaSlabAllocator::new(topology, capacity_bytes);
385 let slab = device.create_buffer(&wgpu::BufferDescriptor {
386 label: Some("forge-slab-coopmat"),
387 size: capacity_bytes as u64,
388 usage: wgpu::BufferUsages::STORAGE
389 | wgpu::BufferUsages::UNIFORM
390 | wgpu::BufferUsages::COPY_DST
391 | wgpu::BufferUsages::COPY_SRC,
392 mapped_at_creation: false,
393 });
394 let out_slab = device.create_buffer(&wgpu::BufferDescriptor {
395 label: Some("forge-out-slab-coopmat"),
396 size: capacity_bytes as u64,
397 usage: wgpu::BufferUsages::STORAGE
398 | wgpu::BufferUsages::COPY_DST
399 | wgpu::BufferUsages::COPY_SRC,
400 mapped_at_creation: false,
401 });
402 let weight_slab = device.create_buffer(&wgpu::BufferDescriptor {
403 label: Some("forge-weight-slab-coopmat"),
404 size: capacity_bytes as u64,
405 usage: wgpu::BufferUsages::STORAGE
406 | wgpu::BufferUsages::COPY_DST
407 | wgpu::BufferUsages::COPY_SRC,
408 mapped_at_creation: false,
409 });
410 let timestamp_resources = timestamp_supported.then(|| TimestampResources::new(&device));
411 return Ok(Self {
412 device,
413 queue,
414 adapter: adapter_id,
415 constraints,
416 profile,
417 allocator,
418 slab,
419 out_slab,
420 weight_slab,
421 weight_cursor: 0,
422 timestamp_supported,
423 timestamp_period_ns,
424 timestamp_resources,
425 pipeline_cache: RefCell::new(HashMap::new()),
426 });
427 }
428
429 log::info!("forge|coopmat_adapter|none_found|falling_back_to_default");
431 Self::new(capacity_bytes)
432 }
433
434 #[cfg(not(target_arch = "wasm32"))]
450 pub fn from_device(
451 device: wgpu::Device,
452 queue: wgpu::Queue,
453 caps: &GpuAdapterCaps,
454 capacity_bytes: usize,
455 ) -> Result<Self, ForgeError> {
456 let features = device.features();
457 let limits = device.limits();
458 let timestamp_supported = features.contains(wgpu::Features::TIMESTAMP_QUERY);
459
460 let mut constraints = AdapterConstraints::from_wgpu_limits(&limits);
462 constraints.supports_subgroups = features.contains(wgpu::Features::SUBGROUP);
463 constraints.supports_coopmat =
464 features.contains(wgpu::Features::EXPERIMENTAL_COOPERATIVE_MATRIX);
465 constraints.supports_rt_cores = features.contains(wgpu::Features::EXPERIMENTAL_RAY_QUERY);
466 constraints.warp_size = if caps.vendor == 0x1002 { 64 } else { 32 };
468
469 let topology = if matches!(
470 caps.device_type,
471 wgpu::DeviceType::IntegratedGpu | wgpu::DeviceType::Cpu
472 ) {
473 MemoryTopology::Unified { zero_copy: true }
474 } else {
475 MemoryTopology::Discrete {
476 staging_required: true,
477 }
478 };
479 let memory_class = match topology {
480 MemoryTopology::Unified { .. } => "unified",
481 MemoryTopology::Discrete { .. } => "discrete",
482 }
483 .to_string();
484
485 let adapter = AdapterIdentity {
486 name: caps.name.clone(),
487 vendor: caps.vendor,
488 device: caps.device,
489 device_type: format!("{:?}", caps.device_type),
490 backend: format!("{:?}", caps.backend),
491 driver: caps.driver.clone(),
492 driver_info: caps.driver_info.clone(),
493 };
494 let timestamp_period_ns = if timestamp_supported {
495 queue.get_timestamp_period()
496 } else {
497 0.0
498 };
499 let profile = HardwareProfile {
500 adapter: adapter.clone(),
501 constraints,
502 memory_class,
503 supports_timestamp_query: timestamp_supported,
504 max_compute_workgroup_storage_size: limits.max_compute_workgroup_storage_size,
505 max_storage_buffer_binding_size: limits.max_storage_buffer_binding_size,
506 min_storage_buffer_offset_alignment: limits.min_storage_buffer_offset_alignment,
507 min_uniform_buffer_offset_alignment: limits.min_uniform_buffer_offset_alignment,
508 };
509
510 let allocator = QualiaSlabAllocator::new(topology, capacity_bytes);
511 let slab = device.create_buffer(&wgpu::BufferDescriptor {
512 label: Some("forge-slab"),
513 size: capacity_bytes as u64,
514 usage: wgpu::BufferUsages::STORAGE
515 | wgpu::BufferUsages::UNIFORM
516 | wgpu::BufferUsages::COPY_DST
517 | wgpu::BufferUsages::COPY_SRC,
518 mapped_at_creation: false,
519 });
520 let out_slab = device.create_buffer(&wgpu::BufferDescriptor {
521 label: Some("forge-out-slab"),
522 size: capacity_bytes as u64,
523 usage: wgpu::BufferUsages::STORAGE
524 | wgpu::BufferUsages::COPY_DST
525 | wgpu::BufferUsages::COPY_SRC,
526 mapped_at_creation: false,
527 });
528 let weight_slab = device.create_buffer(&wgpu::BufferDescriptor {
529 label: Some("forge-weight-slab"),
530 size: capacity_bytes as u64,
531 usage: wgpu::BufferUsages::STORAGE
532 | wgpu::BufferUsages::COPY_DST
533 | wgpu::BufferUsages::COPY_SRC,
534 mapped_at_creation: false,
535 });
536 let timestamp_resources = timestamp_supported.then(|| TimestampResources::new(&device));
537
538 Ok(Self {
539 device,
540 queue,
541 adapter,
542 constraints,
543 profile,
544 allocator,
545 slab,
546 out_slab,
547 weight_slab,
548 weight_cursor: 0,
549 timestamp_supported,
550 timestamp_period_ns,
551 timestamp_resources,
552 pipeline_cache: RefCell::new(HashMap::new()),
553 })
554 }
555
556 fn slab_for(&self, usage: BindingUsage) -> &wgpu::Buffer {
558 match usage {
559 BindingUsage::StorageReadWrite => &self.out_slab,
560 BindingUsage::StorageReadResident => &self.weight_slab,
561 BindingUsage::StorageRead | BindingUsage::Uniform => &self.slab,
562 }
563 }
564
565 pub fn allocate_and_write(
581 &mut self,
582 data: &[u8],
583 binding: u32,
584 group: u32,
585 usage: BindingUsage,
586 ) -> Result<BufferView, ForgeError> {
587 let view = self
588 .allocator
589 .allocate_transient(data.len(), binding, group, usage)?;
590 if !data.is_empty() {
591 let slab = self.slab_for(usage);
592 self.queue
593 .write_buffer(slab, view.offset as wgpu::BufferAddress, data);
594 }
595 Ok(view)
596 }
597
598 pub fn allocate_weight(
605 &mut self,
606 data: &[u8],
607 binding: u32,
608 group: u32,
609 ) -> Result<BufferView, ForgeError> {
610 let offset =
611 self.weight_cursor.div_ceil(DEFAULT_BINDING_ALIGNMENT) * DEFAULT_BINDING_ALIGNMENT;
612 let end = offset + data.len();
613 let cap = self.weight_slab.size() as usize;
614 if end > cap {
615 return Err(ForgeError::GpuValidation(format!(
616 "weight region overflow: need {end} bytes but weight slab is {cap} (raise capacity)"
617 )));
618 }
619 if !data.is_empty() {
620 self.queue
621 .write_buffer(&self.weight_slab, offset as wgpu::BufferAddress, data);
622 }
623 self.weight_cursor = end;
624 Ok(BufferView {
625 offset,
626 length_bytes: data.len(),
627 binding,
628 group,
629 usage: BindingUsage::StorageReadResident,
630 })
631 }
632
633 pub fn clear_weights(&mut self) {
637 self.weight_cursor = 0;
638 }
639
640 pub fn resident_weight_bytes(&self) -> usize {
642 self.weight_cursor
643 }
644
645 pub fn allocate_transient(
646 &mut self,
647 size_bytes: usize,
648 binding: u32,
649 group: u32,
650 usage: BindingUsage,
651 ) -> Result<BufferView, ForgeError> {
652 self.allocator
653 .allocate_transient(size_bytes, binding, group, usage)
654 }
655
656 pub fn advance_read_head(&mut self, offset: usize) {
657 self.allocator.advance_read_head(offset);
658 }
659
660 pub fn clear_transient_allocations(&mut self) {
661 self.allocator.clear();
662 }
663
664 pub fn build_triangle_scene(
676 &self,
677 vertices: &[f32],
678 ) -> Result<(wgpu::Blas, wgpu::Tlas), ForgeError> {
679 if !self.constraints.supports_rt_cores {
680 return Err(ForgeError::GpuUnavailable(
681 "adapter lacks ray-query (RT) support".to_string(),
682 ));
683 }
684 if vertices.is_empty() || vertices.len() % 9 != 0 {
685 return Err(ForgeError::GpuValidation(format!(
686 "triangle scene needs a non-empty multiple of 9 floats (3 verts x xyz); got {}",
687 vertices.len()
688 )));
689 }
690 let vertex_count = (vertices.len() / 3) as u32;
691
692 let vertex_buffer = self.device.create_buffer(&wgpu::BufferDescriptor {
693 label: Some("forge-blas-vertices"),
694 size: (vertices.len() * size_of::<f32>()) as u64,
695 usage: wgpu::BufferUsages::BLAS_INPUT | wgpu::BufferUsages::COPY_DST,
696 mapped_at_creation: false,
697 });
698 self.queue
699 .write_buffer(&vertex_buffer, 0, bytemuck::cast_slice(vertices));
700
701 let size_desc = wgpu::BlasTriangleGeometrySizeDescriptor {
702 vertex_format: wgpu::VertexFormat::Float32x3,
703 vertex_count,
704 index_format: None,
705 index_count: None,
706 flags: wgpu::AccelerationStructureGeometryFlags::OPAQUE,
707 };
708 let blas = self.device.create_blas(
709 &wgpu::CreateBlasDescriptor {
710 label: Some("forge-blas"),
711 flags: wgpu::AccelerationStructureFlags::PREFER_FAST_TRACE,
712 update_mode: wgpu::AccelerationStructureUpdateMode::Build,
713 },
714 wgpu::BlasGeometrySizeDescriptors::Triangles {
715 descriptors: vec![size_desc.clone()],
716 },
717 );
718
719 let mut tlas = self.device.create_tlas(&wgpu::CreateTlasDescriptor {
720 label: Some("forge-tlas"),
721 max_instances: 1,
722 flags: wgpu::AccelerationStructureFlags::PREFER_FAST_TRACE,
723 update_mode: wgpu::AccelerationStructureUpdateMode::Build,
724 });
725 let identity: [f32; 12] = [
727 1.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0,
730 ];
731 tlas[0] = Some(wgpu::TlasInstance::new(&blas, identity, 0, 0xFF));
732
733 let geometry = wgpu::BlasTriangleGeometry {
734 size: &size_desc,
735 vertex_buffer: &vertex_buffer,
736 first_vertex: 0,
737 vertex_stride: (3 * size_of::<f32>()) as wgpu::BufferAddress,
738 index_buffer: None,
739 first_index: None,
740 transform_buffer: None,
741 transform_buffer_offset: None,
742 };
743 let entry = wgpu::BlasBuildEntry {
744 blas: &blas,
745 geometry: wgpu::BlasGeometries::TriangleGeometries(vec![geometry]),
746 };
747 let mut encoder = self
748 .device
749 .create_command_encoder(&wgpu::CommandEncoderDescriptor {
750 label: Some("forge-build-accel"),
751 });
752 encoder.build_acceleration_structures(std::iter::once(&entry), std::iter::once(&tlas));
753 self.queue.submit(Some(encoder.finish()));
754 self.device
755 .poll(wgpu::PollType::wait_indefinitely())
756 .map_err(|e| {
757 ForgeError::DeviceLost(format!("device poll failed building accel: {e:?}"))
758 })?;
759
760 Ok((blas, tlas))
761 }
762
763 pub fn compile_pipeline(
769 &self,
770 source: &str,
771 entry_point: &str,
772 ) -> Result<wgpu::ComputePipeline, ForgeError> {
773 let error_scope = self.device.push_error_scope(wgpu::ErrorFilter::Validation);
774 let shader = self
775 .device
776 .create_shader_module(wgpu::ShaderModuleDescriptor {
777 label: Some("qualia-wgsl-forge"),
778 source: wgpu::ShaderSource::Wgsl(source.into()),
779 });
780 let pipeline = self
781 .device
782 .create_compute_pipeline(&wgpu::ComputePipelineDescriptor {
783 label: Some("qualia-wgsl-forge-pipeline"),
784 layout: None,
785 module: &shader,
786 entry_point: Some(entry_point),
787 compilation_options: Default::default(),
788 cache: None,
789 });
790 if let Some(error) = pollster::block_on(error_scope.pop()) {
791 return Err(ForgeError::GpuValidation(error.to_string()));
792 }
793 Ok(pipeline)
794 }
795
796 pub fn compile_pipeline_spirv(
802 &self,
803 spirv: &[u8],
804 entry_point: &str,
805 ) -> Result<wgpu::ComputePipeline, ForgeError> {
806 let error_scope = self.device.push_error_scope(wgpu::ErrorFilter::Validation);
807 let spirv_words: &[u32] = bytemuck::cast_slice(spirv);
808 let shader = self
809 .device
810 .create_shader_module(wgpu::ShaderModuleDescriptor {
811 label: Some("qualia-forge-spirv"),
812 source: wgpu::ShaderSource::SpirV(std::borrow::Cow::Borrowed(spirv_words)),
813 });
814 let pipeline = self
815 .device
816 .create_compute_pipeline(&wgpu::ComputePipelineDescriptor {
817 label: Some("qualia-forge-spirv-pipeline"),
818 layout: None,
819 module: &shader,
820 entry_point: Some(entry_point),
821 compilation_options: Default::default(),
822 cache: None,
823 });
824 if let Some(error) = pollster::block_on(error_scope.pop()) {
825 return Err(ForgeError::GpuValidation(error.to_string()));
826 }
827 Ok(pipeline)
828 }
829
830 pub fn compile_pipeline_cached(
837 &self,
838 source: &str,
839 entry_point: &str,
840 ) -> Result<wgpu::ComputePipeline, ForgeError> {
841 let key = format!("{entry_point}\u{0}{source}");
842 if let Some(pipeline) = self.pipeline_cache.borrow().get(&key) {
843 return Ok(pipeline.clone());
844 }
845 let pipeline = self.compile_pipeline(source, entry_point)?;
846 self.pipeline_cache
847 .borrow_mut()
848 .insert(key, pipeline.clone());
849 Ok(pipeline)
850 }
851
852 pub fn cached_pipeline_count(&self) -> usize {
854 self.pipeline_cache.borrow().len()
855 }
856
857 pub fn create_compute_bind_group(
861 &self,
862 pipeline: &wgpu::ComputePipeline,
863 buffers: &[BufferView],
864 ) -> wgpu::BindGroup {
865 let mut entries = Vec::with_capacity(buffers.len());
866 for view in buffers {
867 let size = wgpu::BufferSize::new(view.length_bytes as u64);
868 entries.push(wgpu::BindGroupEntry {
869 binding: view.binding,
870 resource: wgpu::BindingResource::Buffer(wgpu::BufferBinding {
871 buffer: self.slab_for(view.usage),
872 offset: view.offset as wgpu::BufferAddress,
873 size,
874 }),
875 });
876 }
877 self.device.create_bind_group(&wgpu::BindGroupDescriptor {
878 label: Some("forge-bind-group"),
879 layout: &pipeline.get_bind_group_layout(0),
880 entries: &entries,
881 })
882 }
883
884 pub fn submit_graph(&self, passes: &[GraphPass]) -> Result<(), ForgeError> {
895 let error_scope = self.device.push_error_scope(wgpu::ErrorFilter::Validation);
896 let mut encoder = self
897 .device
898 .create_command_encoder(&wgpu::CommandEncoderDescriptor {
899 label: Some("forge-graph-encoder"),
900 });
901 for pass in passes {
902 {
903 let mut cpass = encoder.begin_compute_pass(&wgpu::ComputePassDescriptor {
904 label: Some("forge-graph-pass"),
905 timestamp_writes: None,
906 });
907 cpass.set_pipeline(&pass.pipeline);
908 cpass.set_bind_group(0, &pass.bind_group, &[]);
909 cpass.dispatch_workgroups(pass.workgroups, 1, 1);
910 }
911 if let Some((src, dst)) = &pass.copy {
915 let len = src.length_bytes.min(dst.length_bytes) as u64;
916 if len > 0 {
917 encoder.copy_buffer_to_buffer(
918 self.slab_for(src.usage),
919 src.offset as u64,
920 self.slab_for(dst.usage),
921 dst.offset as u64,
922 len,
923 );
924 }
925 }
926 }
927 self.queue.submit(Some(encoder.finish()));
928 self.device
929 .poll(wgpu::PollType::wait_indefinitely())
930 .map_err(|e| {
931 ForgeError::DeviceLost(format!("device poll failed submitting graph: {e:?}"))
932 })?;
933 if let Some(error) = pollster::block_on(error_scope.pop()) {
934 return Err(ForgeError::GpuValidation(error.to_string()));
935 }
936 Ok(())
937 }
938
939 pub fn copy_view(&self, src: &BufferView, dst: &BufferView) -> Result<(), ForgeError> {
946 let len = src.length_bytes.min(dst.length_bytes) as u64;
947 if len == 0 {
948 return Ok(());
949 }
950 let mut encoder = self
951 .device
952 .create_command_encoder(&wgpu::CommandEncoderDescriptor {
953 label: Some("forge-view-copy"),
954 });
955 encoder.copy_buffer_to_buffer(
956 self.slab_for(src.usage),
957 src.offset as u64,
958 self.slab_for(dst.usage),
959 dst.offset as u64,
960 len,
961 );
962 self.queue.submit(Some(encoder.finish()));
963 Ok(())
964 }
965
966 pub fn read_buffer_f32(&self, view: &BufferView) -> Result<Vec<f32>, ForgeError> {
967 let size = view.length_bytes as u64;
968 let staging = self.device.create_buffer(&wgpu::BufferDescriptor {
969 label: Some("forge-output-staging"),
970 size,
971 usage: wgpu::BufferUsages::COPY_DST | wgpu::BufferUsages::MAP_READ,
972 mapped_at_creation: false,
973 });
974 let mut encoder = self
975 .device
976 .create_command_encoder(&wgpu::CommandEncoderDescriptor {
977 label: Some("forge-output-copy"),
978 });
979 encoder.copy_buffer_to_buffer(
980 self.slab_for(view.usage),
981 view.offset as u64,
982 &staging,
983 0,
984 size,
985 );
986 self.queue.submit(Some(encoder.finish()));
987 let bytes = map_read(&self.device, &staging)?;
988
989 let elements = view.length_bytes / size_of::<f32>();
990 let output = bytemuck::cast_slice::<u8, f32>(&bytes)[..elements].to_vec();
991 drop(bytes);
992 staging.unmap();
993 Ok(output)
994 }
995}
996
997impl OracleContext for WgpuComputeContext {
998 fn allocate_and_write(
999 &mut self,
1000 data: &[u8],
1001 binding: u32,
1002 group: u32,
1003 usage: BindingUsage,
1004 ) -> Result<BufferView, ForgeError> {
1005 WgpuComputeContext::allocate_and_write(self, data, binding, group, usage)
1006 }
1007
1008 fn allocate_transient(
1009 &mut self,
1010 size_bytes: usize,
1011 binding: u32,
1012 group: u32,
1013 usage: BindingUsage,
1014 ) -> Result<BufferView, ForgeError> {
1015 WgpuComputeContext::allocate_transient(self, size_bytes, binding, group, usage)
1016 }
1017
1018 fn read_buffer_f32(&self, view: &BufferView) -> Result<Vec<f32>, ForgeError> {
1019 WgpuComputeContext::read_buffer_f32(self, view)
1020 }
1021
1022 fn clear_transient_allocations(&mut self) {
1023 WgpuComputeContext::clear_transient_allocations(self);
1024 }
1025
1026 fn adapter(&self) -> &AdapterIdentity {
1027 &self.adapter
1028 }
1029
1030 fn constraints(&self) -> &AdapterConstraints {
1031 &self.constraints
1032 }
1033
1034 fn timestamp_supported(&self) -> bool {
1035 self.timestamp_supported
1036 }
1037
1038 fn run_kernel(
1042 &mut self,
1043 kernel: &KernelSpec,
1044 schedule: &Schedule,
1045 buffers: &[BufferView],
1046 element_count: usize,
1047 warmups: usize,
1048 samples: usize,
1049 ) -> Result<Vec<u64>, ForgeError> {
1050 let generated = emit_shader(kernel, *schedule, TargetBackend::Wgsl)?;
1051 let pipeline = WgpuPipeline::compile(self, &generated.source, &kernel.entry_point)?;
1052
1053 for _ in 0..warmups {
1054 pipeline.dispatch(buffers, schedule, element_count)?;
1055 }
1056 let mut timing_samples = Vec::with_capacity(samples);
1057 for _ in 0..samples {
1058 timing_samples.push(pipeline.dispatch(buffers, schedule, element_count)?);
1059 }
1060 Ok(timing_samples)
1061 }
1062}
1063
1064pub struct GraphPass {
1070 pub pipeline: wgpu::ComputePipeline,
1071 pub bind_group: wgpu::BindGroup,
1072 pub workgroups: u32,
1073 pub copy: Option<(BufferView, BufferView)>,
1074}
1075
1076pub struct WgpuPipeline<'a> {
1077 context: &'a WgpuComputeContext,
1078 pipeline: wgpu::ComputePipeline,
1079}
1080
1081impl<'a> WgpuPipeline<'a> {
1082 pub fn compile(
1083 context: &'a WgpuComputeContext,
1084 source: &str,
1085 entry_point: &str,
1086 ) -> Result<Self, ForgeError> {
1087 let pipeline = context.compile_pipeline(source, entry_point)?;
1088 Ok(Self { context, pipeline })
1089 }
1090
1091 pub fn compile_spirv(
1093 context: &'a WgpuComputeContext,
1094 spirv: &[u8],
1095 entry_point: &str,
1096 ) -> Result<Self, ForgeError> {
1097 let pipeline = context.compile_pipeline_spirv(spirv, entry_point)?;
1098 Ok(Self { context, pipeline })
1099 }
1100
1101 pub fn compile_msl(
1109 context: &'a WgpuComputeContext,
1110 _source: &str,
1111 _entry_point: &str,
1112 ) -> Result<Self, ForgeError> {
1113 let _ = context;
1118 Err(ForgeError::Emission(
1119 "MSL native compilation requires a metal-rs bridge (not yet implemented). \
1120 Falling back to WGSL."
1121 .to_string(),
1122 ))
1123 }
1124
1125 pub fn dispatch_rayprobe(
1132 &self,
1133 tlas: &wgpu::Tlas,
1134 buffers: &[BufferView],
1135 schedule: &Schedule,
1136 element_count: usize,
1137 ) -> Result<u64, ForgeError> {
1138 let mut entries = Vec::with_capacity(buffers.len() + 1);
1139 entries.push(wgpu::BindGroupEntry {
1140 binding: 0,
1141 resource: tlas.as_binding(),
1142 });
1143 for view in buffers {
1144 let size = wgpu::BufferSize::new(view.length_bytes as u64);
1145 entries.push(wgpu::BindGroupEntry {
1146 binding: view.binding,
1147 resource: wgpu::BindingResource::Buffer(wgpu::BufferBinding {
1148 buffer: self.context.slab_for(view.usage),
1149 offset: view.offset as wgpu::BufferAddress,
1150 size,
1151 }),
1152 });
1153 }
1154 let bind_group = self
1155 .context
1156 .device
1157 .create_bind_group(&wgpu::BindGroupDescriptor {
1158 label: Some("forge-rayprobe-bind-group"),
1159 layout: &self.pipeline.get_bind_group_layout(0),
1160 entries: &entries,
1161 });
1162 let dispatch_x = schedule.dispatch_workgroups(element_count);
1163
1164 let started = Instant::now();
1165 let mut encoder =
1166 self.context
1167 .device
1168 .create_command_encoder(&wgpu::CommandEncoderDescriptor {
1169 label: Some("forge-rayprobe-dispatch"),
1170 });
1171 {
1172 let mut pass = encoder.begin_compute_pass(&wgpu::ComputePassDescriptor {
1173 label: Some("forge-rayprobe-pass"),
1174 timestamp_writes: None,
1175 });
1176 pass.set_pipeline(&self.pipeline);
1177 pass.set_bind_group(0, &bind_group, &[]);
1178 pass.dispatch_workgroups(dispatch_x, 1, 1);
1179 }
1180 self.context.queue.submit(Some(encoder.finish()));
1181 let _ = self
1182 .context
1183 .device
1184 .poll(wgpu::PollType::wait_indefinitely());
1185 Ok(started.elapsed().as_nanos().min(u64::MAX as u128) as u64)
1186 }
1187}
1188
1189impl<'a> QualiaCompute for WgpuPipeline<'a> {
1190 fn dispatch(
1191 &self,
1192 buffers: &[BufferView],
1193 schedule: &Schedule,
1194 element_count: usize,
1195 ) -> Result<u64, ForgeError> {
1196 let bind_group = self
1197 .context
1198 .create_compute_bind_group(&self.pipeline, buffers);
1199 let dispatch_x = schedule.dispatch_workgroups(element_count);
1200
1201 let started = Instant::now();
1202 let mut encoder =
1203 self.context
1204 .device
1205 .create_command_encoder(&wgpu::CommandEncoderDescriptor {
1206 label: Some("forge-dispatch"),
1207 });
1208 {
1209 let timestamp_writes = self.context.timestamp_resources.as_ref().map(|resources| {
1210 wgpu::ComputePassTimestampWrites {
1211 query_set: &resources.query_set,
1212 beginning_of_pass_write_index: Some(0),
1213 end_of_pass_write_index: Some(1),
1214 }
1215 });
1216 let mut pass = encoder.begin_compute_pass(&wgpu::ComputePassDescriptor {
1217 label: Some("forge-compute-pass"),
1218 timestamp_writes,
1219 });
1220 pass.set_pipeline(&self.pipeline);
1221 pass.set_bind_group(0, &bind_group, &[]);
1222 pass.dispatch_workgroups(dispatch_x, 1, 1);
1223 }
1224
1225 if let Some(resources) = &self.context.timestamp_resources {
1226 encoder.resolve_query_set(&resources.query_set, 0..2, &resources.resolve, 0);
1227 encoder.copy_buffer_to_buffer(&resources.resolve, 0, &resources.staging, 0, 16);
1228 }
1229 self.context.queue.submit(Some(encoder.finish()));
1230
1231 if let Some(resources) = &self.context.timestamp_resources {
1232 let bytes = map_read(&self.context.device, &resources.staging)?;
1233 let ticks: &[u64] = bytemuck::cast_slice(&bytes);
1234 let elapsed = ticks
1235 .get(1)
1236 .copied()
1237 .unwrap_or(0)
1238 .saturating_sub(ticks.first().copied().unwrap_or(0));
1239 drop(bytes);
1240 resources.staging.unmap();
1241 Ok((elapsed as f64 * self.context.timestamp_period_ns as f64) as u64)
1242 } else {
1243 self.context
1244 .device
1245 .poll(wgpu::PollType::wait_indefinitely())
1246 .map_err(|e| ForgeError::DeviceLost(format!("device poll failed: {e:?}")))?;
1247 Ok(started.elapsed().as_nanos().min(u64::MAX as u128) as u64)
1248 }
1249 }
1250}
1251
1252struct TimestampResources {
1253 query_set: wgpu::QuerySet,
1254 resolve: wgpu::Buffer,
1255 staging: wgpu::Buffer,
1256}
1257
1258impl TimestampResources {
1259 fn new(device: &wgpu::Device) -> Self {
1260 Self {
1261 query_set: device.create_query_set(&wgpu::QuerySetDescriptor {
1262 label: Some("forge-timestamps"),
1263 ty: wgpu::QueryType::Timestamp,
1264 count: 2,
1265 }),
1266 resolve: device.create_buffer(&wgpu::BufferDescriptor {
1267 label: Some("forge-timestamp-resolve"),
1268 size: 16,
1269 usage: wgpu::BufferUsages::QUERY_RESOLVE | wgpu::BufferUsages::COPY_SRC,
1270 mapped_at_creation: false,
1271 }),
1272 staging: device.create_buffer(&wgpu::BufferDescriptor {
1273 label: Some("forge-timestamp-staging"),
1274 size: 16,
1275 usage: wgpu::BufferUsages::COPY_DST | wgpu::BufferUsages::MAP_READ,
1276 mapped_at_creation: false,
1277 }),
1278 }
1279 }
1280}
1281
1282fn map_read(device: &wgpu::Device, buffer: &wgpu::Buffer) -> Result<wgpu::BufferView, ForgeError> {
1283 let slice = buffer.slice(..);
1284 let (sender, receiver) = mpsc::channel();
1285 slice.map_async(wgpu::MapMode::Read, move |result| {
1286 let _ = sender.send(result);
1287 });
1288 device
1289 .poll(wgpu::PollType::wait_indefinitely())
1290 .map_err(|e| ForgeError::DeviceLost(format!("device poll failed during map: {e:?}")))?;
1291 receiver
1292 .recv()
1293 .map_err(|error| ForgeError::GpuValidation(error.to_string()))?
1294 .map_err(|error| ForgeError::GpuValidation(error.to_string()))?;
1295 slice
1297 .get_mapped_range()
1298 .map_err(|e| ForgeError::GpuValidation(format!("map_range failed: {e:?}")))
1299}