use crate::context::GpuContext;
use crate::error::{GpuError, GpuResult};
use std::collections::HashMap;
use tracing::{debug, info};
#[derive(Debug, Clone)]
pub struct VulkanOptimizationConfig {
pub enable_subgroup_ops: bool,
pub enable_push_constants: bool,
pub enable_timeline_semaphores: bool,
pub descriptor_pool_size: u32,
pub enable_async_compute: bool,
}
impl Default for VulkanOptimizationConfig {
fn default() -> Self {
Self {
enable_subgroup_ops: true,
enable_push_constants: true,
enable_timeline_semaphores: true,
descriptor_pool_size: 1000,
enable_async_compute: true,
}
}
}
pub struct VulkanFeatureDetector {
features: VulkanFeatures,
}
#[derive(Debug, Clone)]
pub struct VulkanFeatures {
pub subgroup_size: u32,
pub subgroup_arithmetic: bool,
pub subgroup_ballot: bool,
pub subgroup_shuffle: bool,
pub timeline_semaphores: bool,
pub max_push_constants_size: u32,
pub async_compute: bool,
}
impl Default for VulkanFeatures {
fn default() -> Self {
Self {
subgroup_size: 32,
subgroup_arithmetic: true,
subgroup_ballot: true,
subgroup_shuffle: true,
timeline_semaphores: true,
max_push_constants_size: 128,
async_compute: true,
}
}
}
impl VulkanFeatureDetector {
pub fn new(context: &GpuContext) -> Self {
let features = Self::detect_features(context);
info!(
"Vulkan features: subgroup_size={}, arithmetic={}, ballot={}, shuffle={}",
features.subgroup_size,
features.subgroup_arithmetic,
features.subgroup_ballot,
features.subgroup_shuffle
);
Self { features }
}
pub fn features(&self) -> &VulkanFeatures {
&self.features
}
fn detect_features(context: &GpuContext) -> VulkanFeatures {
let device_features = context.device().features();
let subgroup = device_features.contains(wgpu::Features::SUBGROUP);
let adapter_info = context.adapter_info();
let subgroup_size = adapter_info.subgroup_max_size.max(1);
VulkanFeatures {
subgroup_size,
subgroup_arithmetic: subgroup,
subgroup_ballot: subgroup,
subgroup_shuffle: subgroup,
timeline_semaphores: true,
max_push_constants_size: 128,
async_compute: true,
}
}
}
pub struct SubgroupOptimizer {
features: VulkanFeatures,
config: VulkanOptimizationConfig,
}
impl SubgroupOptimizer {
pub fn new(features: VulkanFeatures, config: VulkanOptimizationConfig) -> Self {
Self { features, config }
}
pub fn optimize_shader(&self, shader_code: &str) -> String {
if !self.config.enable_subgroup_ops {
return shader_code.to_string();
}
let mut prologue = String::new();
if !shader_code.contains("SUBGROUP_SIZE") {
prologue.push_str(&format!(
"const SUBGROUP_SIZE: u32 = {}u;\n",
self.features.subgroup_size
));
}
let needs_emulation = !self.features.subgroup_arithmetic || !self.features.subgroup_ballot;
if needs_emulation {
prologue.push_str(Self::emulation_scratch_decl());
}
let mut helpers = String::new();
helpers.push_str(&Self::subgroup_arithmetic_helpers(
self.features.subgroup_arithmetic,
));
helpers.push_str(&Self::subgroup_ballot_helpers(
self.features.subgroup_ballot,
));
format!("{prologue}{helpers}\n{shader_code}")
}
pub const EMU_MAX: u32 = 256;
fn emulation_scratch_decl() -> &'static str {
r#"
// Workgroup-shared scratch backing the emulated subgroup helpers.
const SUBGROUP_EMU_MAX: u32 = 256u;
var<workgroup> sg_emu_scratch: array<f32, SUBGROUP_EMU_MAX>;
var<workgroup> sg_emu_flags: array<u32, SUBGROUP_EMU_MAX>;
"#
}
fn subgroup_arithmetic_helpers(native: bool) -> String {
if native {
r#"
// Native subgroup arithmetic (device created with Features::SUBGROUP).
// Reductions/scans run across the hardware subgroup; `lid`/`n` are unused.
fn subgroup_add(value: f32, lid: u32, n: u32) -> f32 { return subgroupAdd(value); }
fn subgroup_mul(value: f32, lid: u32, n: u32) -> f32 { return subgroupMul(value); }
fn subgroup_min(value: f32, lid: u32, n: u32) -> f32 { return subgroupMin(value); }
fn subgroup_max(value: f32, lid: u32, n: u32) -> f32 { return subgroupMax(value); }
fn subgroup_inclusive_add(value: f32, lid: u32, n: u32) -> f32 { return subgroupInclusiveAdd(value); }
fn subgroup_exclusive_add(value: f32, lid: u32, n: u32) -> f32 { return subgroupExclusiveAdd(value); }
"#
.to_string()
} else {
r#"
// Emulated subgroup arithmetic — workgroup-wide reduction/scan via shared
// memory + workgroupBarrier(). Semantics match the native builtins but over
// the whole 1-D workgroup (n active invocations, n <= SUBGROUP_EMU_MAX).
fn subgroup_add(value: f32, lid: u32, n: u32) -> f32 {
sg_emu_scratch[lid] = value;
workgroupBarrier();
var stride = 1u;
loop {
if (stride >= n) { break; }
let idx = lid * stride * 2u;
if (idx + stride < n) {
sg_emu_scratch[idx] = sg_emu_scratch[idx] + sg_emu_scratch[idx + stride];
}
stride = stride * 2u;
workgroupBarrier();
}
let result = sg_emu_scratch[0];
workgroupBarrier();
return result;
}
fn subgroup_mul(value: f32, lid: u32, n: u32) -> f32 {
sg_emu_scratch[lid] = value;
workgroupBarrier();
var stride = 1u;
loop {
if (stride >= n) { break; }
let idx = lid * stride * 2u;
if (idx + stride < n) {
sg_emu_scratch[idx] = sg_emu_scratch[idx] * sg_emu_scratch[idx + stride];
}
stride = stride * 2u;
workgroupBarrier();
}
let result = sg_emu_scratch[0];
workgroupBarrier();
return result;
}
fn subgroup_min(value: f32, lid: u32, n: u32) -> f32 {
sg_emu_scratch[lid] = value;
workgroupBarrier();
var stride = 1u;
loop {
if (stride >= n) { break; }
let idx = lid * stride * 2u;
if (idx + stride < n) {
sg_emu_scratch[idx] = min(sg_emu_scratch[idx], sg_emu_scratch[idx + stride]);
}
stride = stride * 2u;
workgroupBarrier();
}
let result = sg_emu_scratch[0];
workgroupBarrier();
return result;
}
fn subgroup_max(value: f32, lid: u32, n: u32) -> f32 {
sg_emu_scratch[lid] = value;
workgroupBarrier();
var stride = 1u;
loop {
if (stride >= n) { break; }
let idx = lid * stride * 2u;
if (idx + stride < n) {
sg_emu_scratch[idx] = max(sg_emu_scratch[idx], sg_emu_scratch[idx + stride]);
}
stride = stride * 2u;
workgroupBarrier();
}
let result = sg_emu_scratch[0];
workgroupBarrier();
return result;
}
fn subgroup_inclusive_add(value: f32, lid: u32, n: u32) -> f32 {
sg_emu_scratch[lid] = value;
workgroupBarrier();
var acc = 0.0;
for (var i = 0u; i <= lid; i = i + 1u) {
acc = acc + sg_emu_scratch[i];
}
workgroupBarrier();
return acc;
}
fn subgroup_exclusive_add(value: f32, lid: u32, n: u32) -> f32 {
sg_emu_scratch[lid] = value;
workgroupBarrier();
var acc = 0.0;
for (var i = 0u; i < lid; i = i + 1u) {
acc = acc + sg_emu_scratch[i];
}
workgroupBarrier();
return acc;
}
"#
.to_string()
}
}
fn subgroup_ballot_helpers(native: bool) -> String {
if native {
r#"
// Native subgroup ballot / vote (device created with Features::SUBGROUP).
fn subgroup_all(predicate: bool, lid: u32, n: u32) -> bool { return subgroupAll(predicate); }
fn subgroup_any(predicate: bool, lid: u32, n: u32) -> bool { return subgroupAny(predicate); }
fn subgroup_ballot(predicate: bool, lid: u32, n: u32) -> u32 {
let b = subgroupBallot(predicate);
return countOneBits(b.x) + countOneBits(b.y) + countOneBits(b.z) + countOneBits(b.w);
}
"#
.to_string()
} else {
r#"
// Emulated subgroup ballot / vote — workgroup-wide via shared flags buffer.
fn subgroup_all(predicate: bool, lid: u32, n: u32) -> bool {
sg_emu_flags[lid] = select(0u, 1u, predicate);
workgroupBarrier();
var acc = 1u;
for (var i = 0u; i < n; i = i + 1u) { acc = acc & sg_emu_flags[i]; }
workgroupBarrier();
return acc != 0u;
}
fn subgroup_any(predicate: bool, lid: u32, n: u32) -> bool {
sg_emu_flags[lid] = select(0u, 1u, predicate);
workgroupBarrier();
var acc = 0u;
for (var i = 0u; i < n; i = i + 1u) { acc = acc | sg_emu_flags[i]; }
workgroupBarrier();
return acc != 0u;
}
fn subgroup_ballot(predicate: bool, lid: u32, n: u32) -> u32 {
sg_emu_flags[lid] = select(0u, 1u, predicate);
workgroupBarrier();
var acc = 0u;
for (var i = 0u; i < n; i = i + 1u) { acc = acc + sg_emu_flags[i]; }
workgroupBarrier();
return acc;
}
"#
.to_string()
}
}
}
pub struct PushConstantsManager {
max_size: u32,
constants: HashMap<String, PushConstant>,
}
#[derive(Debug, Clone)]
struct PushConstant {
name: String,
offset: u32,
size: u32,
data: Vec<u8>,
}
impl PushConstantsManager {
pub fn new(max_size: u32) -> Self {
Self {
max_size,
constants: HashMap::new(),
}
}
pub fn register(&mut self, name: String, size: u32) -> GpuResult<()> {
let offset = self.calculate_next_offset();
if offset + size > self.max_size {
return Err(GpuError::invalid_buffer(format!(
"Push constant exceeds maximum size: {} + {} > {}",
offset, size, self.max_size
)));
}
self.constants.insert(
name.clone(),
PushConstant {
name,
offset,
size,
data: vec![0; size as usize],
},
);
Ok(())
}
pub fn update(&mut self, name: &str, data: &[u8]) -> GpuResult<()> {
let constant = self
.constants
.get_mut(name)
.ok_or_else(|| GpuError::invalid_buffer("Push constant not found"))?;
if data.len() != constant.size as usize {
return Err(GpuError::invalid_buffer("Data size mismatch"));
}
constant.data.copy_from_slice(data);
debug!("Updated push constant '{}' ({} bytes)", name, data.len());
Ok(())
}
pub fn total_size(&self) -> u32 {
self.constants.values().map(|c| c.size).sum()
}
fn calculate_next_offset(&self) -> u32 {
self.constants
.values()
.map(|c| c.offset + c.size)
.max()
.unwrap_or(0)
}
}
pub struct DescriptorSetPool {
pool_size: u32,
allocated: u32,
free_sets: Vec<u32>,
}
impl DescriptorSetPool {
pub fn new(pool_size: u32) -> Self {
Self {
pool_size,
allocated: 0,
free_sets: Vec::new(),
}
}
pub fn allocate(&mut self) -> GpuResult<u32> {
if let Some(set_id) = self.free_sets.pop() {
debug!("Reused descriptor set {}", set_id);
return Ok(set_id);
}
if self.allocated >= self.pool_size {
return Err(GpuError::internal(
"Descriptor set pool exhausted".to_string(),
));
}
let set_id = self.allocated;
self.allocated += 1;
debug!("Allocated descriptor set {}", set_id);
Ok(set_id)
}
pub fn free(&mut self, set_id: u32) {
if set_id < self.allocated {
self.free_sets.push(set_id);
debug!("Freed descriptor set {}", set_id);
}
}
pub fn reset(&mut self) {
self.free_sets.clear();
for i in 0..self.allocated {
self.free_sets.push(i);
}
debug!("Reset descriptor set pool");
}
pub fn stats(&self) -> (u32, u32, usize) {
(self.pool_size, self.allocated, self.free_sets.len())
}
}
pub struct TimelineSemaphoreManager {
semaphores: HashMap<u32, TimelineSemaphore>,
next_id: u32,
}
#[derive(Debug, Clone)]
struct TimelineSemaphore {
id: u32,
value: u64,
name: String,
}
impl TimelineSemaphoreManager {
pub fn new() -> Self {
Self {
semaphores: HashMap::new(),
next_id: 0,
}
}
pub fn create(&mut self, name: String, initial_value: u64) -> u32 {
let id = self.next_id;
self.next_id += 1;
self.semaphores.insert(
id,
TimelineSemaphore {
id,
value: initial_value,
name: name.clone(),
},
);
debug!("Created timeline semaphore '{}' (ID: {})", name, id);
id
}
pub fn signal(&mut self, id: u32, value: u64) -> GpuResult<()> {
let sem = self
.semaphores
.get_mut(&id)
.ok_or_else(|| GpuError::internal("Semaphore not found"))?;
sem.value = value;
debug!("Signaled semaphore '{}' with value {}", sem.name, value);
Ok(())
}
pub fn wait(&self, id: u32, value: u64) -> GpuResult<bool> {
let sem = self
.semaphores
.get(&id)
.ok_or_else(|| GpuError::internal("Semaphore not found"))?;
Ok(sem.value >= value)
}
pub fn get_value(&self, id: u32) -> GpuResult<u64> {
let sem = self
.semaphores
.get(&id)
.ok_or_else(|| GpuError::internal("Semaphore not found"))?;
Ok(sem.value)
}
pub fn destroy(&mut self, id: u32) {
if let Some(sem) = self.semaphores.remove(&id) {
debug!("Destroyed timeline semaphore '{}'", sem.name);
}
}
}
impl Default for TimelineSemaphoreManager {
fn default() -> Self {
Self::new()
}
}
pub struct AsyncComputeQueue {
compute_queue: Option<QueueHandle>,
graphics_queue: Option<QueueHandle>,
transfer_queue: Option<QueueHandle>,
}
#[derive(Debug, Clone)]
struct QueueHandle {
family_index: u32,
queue_index: u32,
}
impl AsyncComputeQueue {
pub fn new() -> Self {
Self {
compute_queue: Some(QueueHandle {
family_index: 0,
queue_index: 0,
}),
graphics_queue: Some(QueueHandle {
family_index: 0,
queue_index: 0,
}),
transfer_queue: None,
}
}
pub fn is_available(&self) -> bool {
self.compute_queue.is_some()
}
pub fn submit_compute(&self, _commands: &[u8]) -> GpuResult<()> {
if self.compute_queue.is_none() {
return Err(GpuError::unsupported_operation(
"Compute queue not available".to_string(),
));
}
Err(GpuError::unsupported_operation(
"async Vulkan compute-queue submission is not implemented on the wgpu backend; \
use GpuContext/ComputePipeline for real GPU execution"
.to_string(),
))
}
pub fn submit_graphics(&self, _commands: &[u8]) -> GpuResult<()> {
if self.graphics_queue.is_none() {
return Err(GpuError::unsupported_operation(
"Graphics queue not available".to_string(),
));
}
Err(GpuError::unsupported_operation(
"async Vulkan graphics-queue submission is not implemented on the wgpu backend; \
use GpuContext/ComputePipeline for real GPU execution"
.to_string(),
))
}
pub fn submit_transfer(&self, _commands: &[u8]) -> GpuResult<()> {
if self.transfer_queue.is_none() {
return self.submit_graphics(_commands);
}
Err(GpuError::unsupported_operation(
"async Vulkan transfer-queue submission is not implemented on the wgpu backend; \
use GpuContext/ComputePipeline for real GPU execution"
.to_string(),
))
}
}
impl Default for AsyncComputeQueue {
fn default() -> Self {
Self::new()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_vulkan_features() {
let features = VulkanFeatures::default();
assert_eq!(features.subgroup_size, 32);
assert!(features.subgroup_arithmetic);
assert!(features.subgroup_ballot);
}
#[test]
fn test_push_constants_manager() {
let mut manager = PushConstantsManager::new(256);
manager
.register("view_matrix".to_string(), 64)
.expect("Failed to register");
manager
.register("light_pos".to_string(), 16)
.expect("Failed to register");
let data = vec![0u8; 64];
manager
.update("view_matrix", &data)
.expect("Failed to update");
assert!(manager.total_size() <= 256);
}
#[test]
fn test_descriptor_set_pool() {
let mut pool = DescriptorSetPool::new(10);
let set1 = pool.allocate().expect("Failed to allocate");
let _set2 = pool.allocate().expect("Failed to allocate");
pool.free(set1);
let set3 = pool.allocate().expect("Failed to allocate");
assert_eq!(set3, set1);
let (pool_size, allocated, free) = pool.stats();
assert_eq!(pool_size, 10);
assert_eq!(allocated, 2);
assert_eq!(free, 0);
}
#[test]
fn test_timeline_semaphore() {
let mut manager = TimelineSemaphoreManager::new();
let sem = manager.create("test_sem".to_string(), 0);
manager.signal(sem, 5).expect("Failed to signal");
assert_eq!(manager.get_value(sem).expect("Failed to get value"), 5);
assert!(manager.wait(sem, 3).expect("Failed to wait"));
assert!(manager.wait(sem, 5).expect("Failed to wait"));
}
#[test]
fn test_async_compute_queue() {
let queue = AsyncComputeQueue::new();
assert!(queue.is_available());
let commands = vec![0u8; 64];
let compute_err = queue.submit_compute(&commands);
assert!(
matches!(compute_err, Err(GpuError::UnsupportedOperation { .. })),
"submit_compute must return an explicit unsupported-operation error, got {compute_err:?}"
);
let graphics_err = queue.submit_graphics(&commands);
assert!(
matches!(graphics_err, Err(GpuError::UnsupportedOperation { .. })),
"submit_graphics must return an explicit unsupported-operation error, got {graphics_err:?}"
);
let transfer_err = queue.submit_transfer(&commands);
assert!(
matches!(transfer_err, Err(GpuError::UnsupportedOperation { .. })),
"submit_transfer must return an explicit unsupported-operation error, got {transfer_err:?}"
);
}
}