Skip to main content

agpu/
context.rs

1//! GPU context — Vulkan-first device initialisation and resource factory.
2//!
3//! `GpuContext` owns the GPU device stack and provides creation methods
4//! that return agpu resource types.  agpu **prefers Vulkan** by default,
5//! falling back to the platform backend when Vulkan is unavailable.
6
7use crate::ontology::*;
8use crate::resource::*;
9use crate::types::*;
10
11/// Core GPU context — instance, adapter, device, and queue.
12///
13/// All resource creation goes through this context so that every
14/// allocated object is an agpu type with tracking metadata.
15pub struct GpuContext {
16    instance: wgpu::Instance,
17    adapter: wgpu::Adapter,
18    device: wgpu::Device,
19    queue: wgpu::Queue,
20    backend_preference: BackendPreference,
21}
22
23impl GpuContext {
24    /// Create a GPU context for the given surface.
25    ///
26    /// Uses Vulkan-first backend selection: tries `Backends::VULKAN`,
27    /// and if no suitable adapter is found, falls back to the platform
28    /// default (`Backends::PRIMARY`).
29    pub async fn new(
30        surface: &wgpu::Surface<'_>,
31        preference: BackendPreference,
32    ) -> Result<Self, GpuError> {
33        let backends = preference.to_backends();
34
35        let instance = wgpu::Instance::new(&wgpu::InstanceDescriptor {
36            backends,
37            ..Default::default()
38        });
39
40        // Try preferred backend first
41        let (instance, adapter) = match instance
42            .request_adapter(&wgpu::RequestAdapterOptions {
43                power_preference: wgpu::PowerPreference::HighPerformance,
44                compatible_surface: Some(surface),
45                force_fallback_adapter: false,
46            })
47            .await
48        {
49            Some(a) => (instance, a),
50            None if matches!(preference, BackendPreference::VulkanPreferred) => {
51                // Vulkan unavailable — fall back to platform default
52                log::info!("agpu: Vulkan adapter not found, falling back to platform default");
53                let fallback_instance = wgpu::Instance::new(&wgpu::InstanceDescriptor {
54                    backends: wgpu::Backends::PRIMARY,
55                    ..Default::default()
56                });
57                let adapter = fallback_instance
58                    .request_adapter(&wgpu::RequestAdapterOptions {
59                        power_preference: wgpu::PowerPreference::HighPerformance,
60                        compatible_surface: Some(surface),
61                        force_fallback_adapter: false,
62                    })
63                    .await
64                    .ok_or(GpuError::NoAdapter)?;
65                (fallback_instance, adapter)
66            }
67            None if matches!(preference, BackendPreference::OpenGLPreferred) => {
68                // OpenGL unavailable — fall back to platform default
69                log::info!("agpu: OpenGL adapter not found, falling back to platform default");
70                let fallback_instance = wgpu::Instance::new(&wgpu::InstanceDescriptor {
71                    backends: wgpu::Backends::PRIMARY,
72                    ..Default::default()
73                });
74                let adapter = fallback_instance
75                    .request_adapter(&wgpu::RequestAdapterOptions {
76                        power_preference: wgpu::PowerPreference::HighPerformance,
77                        compatible_surface: Some(surface),
78                        force_fallback_adapter: false,
79                    })
80                    .await
81                    .ok_or(GpuError::NoAdapter)?;
82                (fallback_instance, adapter)
83            }
84            None => return Err(GpuError::NoAdapter),
85        };
86
87        // Request all features the adapter supports (Vulkan 1.3+ features
88        // are enabled automatically when the adapter exposes them).
89        let supported_features = adapter.features();
90
91        let (device, queue) = adapter
92            .request_device(
93                &wgpu::DeviceDescriptor {
94                    label: Some("agpu_device"),
95                    required_features: supported_features,
96                    required_limits: adapter.limits(),
97                    memory_hints: wgpu::MemoryHints::Performance,
98                },
99                None,
100            )
101            .await
102            .map_err(|e| GpuError::DeviceRequest(e.to_string()))?;
103
104        log::info!(
105            "agpu: GPU initialised — {} ({:?}, {:?})",
106            adapter.get_info().name,
107            adapter.get_info().backend,
108            adapter.get_info().device_type,
109        );
110
111        Ok(Self {
112            instance,
113            adapter,
114            device,
115            queue,
116            backend_preference: preference,
117        })
118    }
119
120    /// Create a GPU context reusing an existing `wgpu::Instance` and surface.
121    ///
122    /// Use this when the caller already owns the instance that created
123    /// the surface — avoids the "Surface does not exist" panic that
124    /// occurs when a *second* instance tries to look up the surface.
125    pub async fn from_instance(
126        instance: wgpu::Instance,
127        surface: &wgpu::Surface<'_>,
128        preference: BackendPreference,
129    ) -> Result<Self, GpuError> {
130        let adapter = instance
131            .request_adapter(&wgpu::RequestAdapterOptions {
132                power_preference: wgpu::PowerPreference::HighPerformance,
133                compatible_surface: Some(surface),
134                force_fallback_adapter: false,
135            })
136            .await;
137
138        // Fallback: if preferred backend yielded nothing, try PRIMARY
139        let (instance, adapter) = match adapter {
140            Some(a) => (instance, a),
141            None if matches!(
142                preference,
143                BackendPreference::VulkanPreferred | BackendPreference::OpenGLPreferred
144            ) =>
145            {
146                log::info!("agpu: preferred adapter not found, falling back to platform default");
147                let fallback = wgpu::Instance::new(&wgpu::InstanceDescriptor {
148                    backends: wgpu::Backends::PRIMARY,
149                    ..Default::default()
150                });
151                let a = fallback
152                    .request_adapter(&wgpu::RequestAdapterOptions {
153                        power_preference: wgpu::PowerPreference::HighPerformance,
154                        compatible_surface: None, // surface belongs to original instance
155                        force_fallback_adapter: false,
156                    })
157                    .await
158                    .ok_or(GpuError::NoAdapter)?;
159                (fallback, a)
160            }
161            None => return Err(GpuError::NoAdapter),
162        };
163
164        let supported_features = adapter.features();
165        let (device, queue) = adapter
166            .request_device(
167                &wgpu::DeviceDescriptor {
168                    label: Some("agpu_device"),
169                    required_features: supported_features,
170                    required_limits: adapter.limits(),
171                    memory_hints: wgpu::MemoryHints::Performance,
172                },
173                None,
174            )
175            .await
176            .map_err(|e| GpuError::DeviceRequest(e.to_string()))?;
177
178        log::info!(
179            "agpu: GPU initialised — {} ({:?}, {:?})",
180            adapter.get_info().name,
181            adapter.get_info().backend,
182            adapter.get_info().device_type,
183        );
184
185        Ok(Self {
186            instance,
187            adapter,
188            device,
189            queue,
190            backend_preference: preference,
191        })
192    }
193
194    // ── Accessors ───────────────────────────────────────────────────
195
196    /// The wgpu device (needed internally and for advanced escape-hatch).
197    pub fn device(&self) -> &wgpu::Device {
198        &self.device
199    }
200    /// The wgpu queue.
201    pub fn queue(&self) -> &wgpu::Queue {
202        &self.queue
203    }
204    /// The wgpu adapter.
205    pub fn adapter(&self) -> &wgpu::Adapter {
206        &self.adapter
207    }
208    /// The wgpu instance.
209    pub fn instance(&self) -> &wgpu::Instance {
210        &self.instance
211    }
212
213    /// Adapter name (e.g. "NVIDIA GeForce RTX 4090").
214    pub fn adapter_name(&self) -> String {
215        self.adapter.get_info().name
216    }
217    /// Active backend (e.g. "Vulkan", "DX12", "Metal").
218    pub fn backend(&self) -> String {
219        format!("{:?}", self.adapter.get_info().backend)
220    }
221    /// Device type (discrete, integrated, software).
222    pub fn device_type(&self) -> String {
223        format!("{:?}", self.adapter.get_info().device_type)
224    }
225    /// Driver name.
226    pub fn driver(&self) -> String {
227        self.adapter.get_info().driver
228    }
229    /// Driver info string.
230    pub fn driver_info(&self) -> String {
231        self.adapter.get_info().driver_info
232    }
233    /// Maximum 2D texture dimension.
234    pub fn max_texture_dimension(&self) -> u32 {
235        self.device.limits().max_texture_dimension_2d
236    }
237    /// Maximum buffer size in bytes.
238    pub fn max_buffer_size(&self) -> u64 {
239        self.device.limits().max_buffer_size
240    }
241    /// Enabled GPU features.
242    pub fn features(&self) -> Features {
243        self.device.features()
244    }
245    /// Device limits.
246    pub fn limits(&self) -> Limits {
247        self.device.limits()
248    }
249    /// Backend preference that was requested.
250    pub fn backend_preference(&self) -> BackendPreference {
251        self.backend_preference
252    }
253
254    // ── Resource creation ───────────────────────────────────────────
255
256    /// Create a GPU buffer.
257    pub fn create_buffer(&self, desc: &GpuBufferDescriptor) -> GpuBuffer {
258        let inner = self.device.create_buffer(&wgpu::BufferDescriptor {
259            label: desc.label.as_deref(),
260            size: desc.size,
261            usage: desc.usage,
262            mapped_at_creation: desc.mapped_at_creation,
263        });
264        GpuBuffer::from_raw(inner, desc)
265    }
266
267    /// Create a GPU texture.
268    pub fn create_texture(&self, desc: &GpuTextureDescriptor) -> GpuTexture {
269        let inner = self.device.create_texture(&wgpu::TextureDescriptor {
270            label: desc.label.as_deref(),
271            size: desc.size,
272            mip_level_count: desc.mip_level_count,
273            sample_count: desc.sample_count,
274            dimension: desc.dimension,
275            format: desc.format,
276            usage: desc.usage,
277            view_formats: &desc.view_formats,
278        });
279        GpuTexture::from_raw(inner, desc)
280    }
281
282    /// Create a GPU sampler.
283    pub fn create_sampler(&self, desc: &GpuSamplerDescriptor) -> GpuSampler {
284        let inner = self.device.create_sampler(&wgpu::SamplerDescriptor {
285            label: desc.label.as_deref(),
286            address_mode_u: desc.address_mode_u,
287            address_mode_v: desc.address_mode_v,
288            address_mode_w: desc.address_mode_w,
289            mag_filter: desc.mag_filter,
290            min_filter: desc.min_filter,
291            mipmap_filter: desc.mipmap_filter,
292            compare: desc.compare,
293            anisotropy_clamp: desc.anisotropy_clamp,
294            lod_min_clamp: desc.lod_min_clamp,
295            lod_max_clamp: desc.lod_max_clamp,
296            ..Default::default()
297        });
298        GpuSampler::from_raw(inner, desc.label.clone())
299    }
300
301    /// Create a shader module from WGSL source.
302    pub fn create_shader_wgsl(&self, label: &str, source: &str) -> GpuShaderModule {
303        let inner = self
304            .device
305            .create_shader_module(wgpu::ShaderModuleDescriptor {
306                label: Some(label),
307                source: wgpu::ShaderSource::Wgsl(source.into()),
308            });
309        GpuShaderModule::from_raw(inner, Some(label.into()), ShaderSourceKind::Wgsl)
310    }
311
312    /// Create a bind group layout.
313    pub fn create_bind_group_layout(
314        &self,
315        label: &str,
316        entries: &[wgpu::BindGroupLayoutEntry],
317    ) -> GpuBindGroupLayout {
318        let inner = self
319            .device
320            .create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
321                label: Some(label),
322                entries,
323            });
324        GpuBindGroupLayout::from_raw(inner, Some(label.into()))
325    }
326
327    /// Create a bind group.
328    pub fn create_bind_group(
329        &self,
330        label: &str,
331        layout: &GpuBindGroupLayout,
332        entries: &[wgpu::BindGroupEntry<'_>],
333    ) -> GpuBindGroup {
334        let inner = self.device.create_bind_group(&wgpu::BindGroupDescriptor {
335            label: Some(label),
336            layout: layout.raw(),
337            entries,
338        });
339        GpuBindGroup::from_raw(inner, Some(label.into()))
340    }
341
342    /// Create a pipeline layout.
343    pub fn create_pipeline_layout(
344        &self,
345        label: &str,
346        bind_group_layouts: &[&GpuBindGroupLayout],
347        push_constant_ranges: &[wgpu::PushConstantRange],
348    ) -> GpuPipelineLayout {
349        let raw_layouts: Vec<&wgpu::BindGroupLayout> =
350            bind_group_layouts.iter().map(|l| l.raw()).collect();
351        let inner = self
352            .device
353            .create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
354                label: Some(label),
355                bind_group_layouts: &raw_layouts,
356                push_constant_ranges,
357            });
358        GpuPipelineLayout::from_raw(inner, Some(label.into()))
359    }
360
361    /// Create a render pipeline.
362    pub fn create_render_pipeline(
363        &self,
364        desc: &wgpu::RenderPipelineDescriptor<'_>,
365    ) -> GpuRenderPipeline {
366        let label = desc.label.map(String::from);
367        let inner = self.device.create_render_pipeline(desc);
368        GpuRenderPipeline::from_raw(inner, label)
369    }
370
371    /// Create a compute pipeline.
372    pub fn create_compute_pipeline(
373        &self,
374        desc: &wgpu::ComputePipelineDescriptor<'_>,
375    ) -> GpuComputePipeline {
376        let label = desc.label.map(String::from);
377        let inner = self.device.create_compute_pipeline(desc);
378        GpuComputePipeline::from_raw(inner, label)
379    }
380
381    /// Create a command encoder.
382    pub fn create_command_encoder(&self, label: &str) -> crate::command::GpuCommandEncoder {
383        let inner = self
384            .device
385            .create_command_encoder(&wgpu::CommandEncoderDescriptor { label: Some(label) });
386        crate::command::GpuCommandEncoder::from_raw(inner)
387    }
388
389    /// Create a query set for timestamp or occlusion queries.
390    pub fn create_query_set(&self, label: &str, ty: wgpu::QueryType, count: u32) -> GpuQuerySet {
391        let inner = self.device.create_query_set(&wgpu::QuerySetDescriptor {
392            label: Some(label),
393            ty,
394            count,
395        });
396        GpuQuerySet::from_raw(inner, Some(label.into()), count)
397    }
398
399    /// Write data to a buffer.
400    pub fn write_buffer(&self, buffer: &GpuBuffer, offset: u64, data: &[u8]) {
401        self.queue.write_buffer(buffer.raw(), offset, data);
402    }
403
404    /// Submit finished command buffers to the GPU.
405    pub fn submit(&self, commands: impl IntoIterator<Item = crate::command::GpuCommandBuffer>) {
406        self.queue
407            .submit(commands.into_iter().map(|c| c.into_inner()));
408    }
409
410    /// Write data directly to a texture.
411    pub fn write_texture(
412        &self,
413        texture: wgpu::TexelCopyTextureInfo<'_>,
414        data: &[u8],
415        data_layout: wgpu::TexelCopyBufferLayout,
416        size: Extent3d,
417    ) {
418        self.queue.write_texture(texture, data, data_layout, size);
419    }
420}
421
422/// Errors from GPU initialisation.
423#[derive(Debug)]
424pub enum GpuError {
425    NoAdapter,
426    DeviceRequest(String),
427    SurfaceConfig(String),
428}
429
430impl std::fmt::Display for GpuError {
431    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
432        match self {
433            Self::NoAdapter => write!(f, "No suitable GPU adapter found"),
434            Self::DeviceRequest(e) => write!(f, "GPU device request failed: {e}"),
435            Self::SurfaceConfig(e) => write!(f, "Surface configuration failed: {e}"),
436        }
437    }
438}
439
440impl std::error::Error for GpuError {}
441
442// ── Ontology ────────────────────────────────────────────────────────
443
444impl Discoverable for GpuContext {
445    fn schema(&self) -> WidgetSchema {
446        let mut schema = WidgetSchema::new(
447            "GpuContext",
448            "Vulkan-first GPU device context — adapter, device, queue, and hardware capabilities",
449            SemanticRole::System,
450        );
451        schema.usage_hint =
452            Some("GpuContext::new(&surface, BackendPreference::default()).await".into());
453        schema.tags = vec![
454            "gpu".into(),
455            "vulkan".into(),
456            "device".into(),
457            "adapter".into(),
458            "hardware".into(),
459        ];
460        schema
461    }
462
463    fn capabilities(&self) -> Vec<AgentCapability> {
464        vec![
465            AgentCapability::Custom("gpu_compute".into()),
466            AgentCapability::Custom("gpu_render".into()),
467            AgentCapability::Custom("vulkan_preferred".into()),
468        ]
469    }
470
471    fn actions(&self) -> Vec<AgentAction> {
472        vec![
473            AgentAction::simple(
474                "get_adapter_info",
475                "Query GPU adapter name, backend, and device type",
476                false,
477            ),
478            AgentAction::simple(
479                "get_limits",
480                "Query device limits (max texture size, buffer size, etc.)",
481                false,
482            ),
483            AgentAction::simple("get_features", "List enabled GPU features", false),
484        ]
485    }
486
487    fn semantic_role(&self) -> SemanticRole {
488        SemanticRole::System
489    }
490
491    fn agent_state(&self) -> serde_json::Value {
492        let info = self.adapter.get_info();
493        let limits = self.device.limits();
494        serde_json::json!({
495            "adapter_name": info.name,
496            "backend": format!("{:?}", info.backend),
497            "device_type": format!("{:?}", info.device_type),
498            "driver": info.driver,
499            "driver_info": info.driver_info,
500            "backend_preference": format!("{:?}", self.backend_preference),
501            "limits": {
502                "max_texture_dimension_2d": limits.max_texture_dimension_2d,
503                "max_buffer_size": limits.max_buffer_size,
504                "max_bind_groups": limits.max_bind_groups,
505                "max_vertex_buffers": limits.max_vertex_buffers,
506                "max_vertex_attributes": limits.max_vertex_attributes,
507                "max_compute_workgroup_size_x": limits.max_compute_workgroup_size_x,
508                "max_compute_workgroups_per_dimension": limits.max_compute_workgroups_per_dimension,
509            }
510        })
511    }
512
513    fn execute_action(
514        &mut self,
515        action: &str,
516        _params: &serde_json::Value,
517    ) -> Result<serde_json::Value, String> {
518        match action {
519            "get_adapter_info" => Ok(serde_json::json!({
520                "name": self.adapter.get_info().name,
521                "backend": format!("{:?}", self.adapter.get_info().backend),
522                "device_type": format!("{:?}", self.adapter.get_info().device_type),
523                "driver": self.adapter.get_info().driver,
524            })),
525            "get_limits" => {
526                let l = self.device.limits();
527                Ok(serde_json::json!({
528                    "max_texture_dimension_2d": l.max_texture_dimension_2d,
529                    "max_buffer_size": l.max_buffer_size,
530                    "max_bind_groups": l.max_bind_groups,
531                }))
532            }
533            "get_features" => {
534                let features = self.adapter.features();
535                Ok(serde_json::json!({
536                    "features": format!("{features:?}"),
537                }))
538            }
539            _ => Err(format!("Unknown action: {action}")),
540        }
541    }
542
543    fn agent_id(&self) -> Option<&str> {
544        Some("gpu_context")
545    }
546
547    fn accessibility_label(&self) -> Option<String> {
548        Some(format!("GPU: {} ({})", self.adapter_name(), self.backend()))
549    }
550}