use crate::error::{IffError, Result};
use crate::format::IffImage;
#[cfg(feature = "gpu")]
use wgpu::util::DeviceExt;
#[cfg(feature = "gpu")]
pub struct GpuDecoder {
device: wgpu::Device,
queue: wgpu::Queue,
inverse_wavelet_pipeline: wgpu::ComputePipeline,
noise_synthesis_pipeline: wgpu::ComputePipeline,
warp_field_pipeline: wgpu::ComputePipeline,
apply_warp_pipeline: wgpu::ComputePipeline,
apply_residual_pipeline: wgpu::ComputePipeline,
composite_pipeline: wgpu::ComputePipeline,
}
#[cfg(feature = "gpu")]
impl GpuDecoder {
pub async fn new() -> Result<Self> {
let instance = wgpu::Instance::new(wgpu::InstanceDescriptor {
backends: wgpu::Backends::all(),
..Default::default()
});
let adapter = instance
.request_adapter(&wgpu::RequestAdapterOptions {
power_preference: wgpu::PowerPreference::HighPerformance,
compatible_surface: None,
force_fallback_adapter: false,
})
.await
.ok_or_else(|| IffError::Other("Failed to find GPU adapter".to_string()))?;
let (device, queue) = adapter
.request_device(
&wgpu::DeviceDescriptor {
label: Some("PPF-IFF GPU Decoder"),
required_features: wgpu::Features::empty(),
required_limits: wgpu::Limits::default(),
},
None,
)
.await
.map_err(|e| IffError::Other(format!("Failed to create device: {}", e)))?;
let inverse_wavelet_pipeline = Self::create_inverse_wavelet_pipeline(&device)?;
let noise_synthesis_pipeline = Self::create_noise_synthesis_pipeline(&device)?;
let warp_field_pipeline = Self::create_warp_field_pipeline(&device)?;
let apply_warp_pipeline = Self::create_apply_warp_pipeline(&device)?;
let apply_residual_pipeline = Self::create_apply_residual_pipeline(&device)?;
let composite_pipeline = Self::create_composite_pipeline(&device)?;
Ok(GpuDecoder {
device,
queue,
inverse_wavelet_pipeline,
noise_synthesis_pipeline,
warp_field_pipeline,
apply_warp_pipeline,
apply_residual_pipeline,
composite_pipeline,
})
}
pub fn decode(&self, iff_image: &IffImage) -> Result<Vec<u8>> {
let width = iff_image.header.width;
let height = iff_image.header.height;
let pixel_count = (width * height) as usize;
let layer1_buffer = self.create_layer1_buffer(iff_image)?;
let layer2_buffer = self.create_layer2_buffer(iff_image)?;
let layer3_buffer = self.create_layer3_buffer(iff_image)?;
let residual_buffer = self.create_residual_buffer(iff_image)?;
let reconstruction_buffer = self.device.create_buffer(&wgpu::BufferDescriptor {
label: Some("Reconstruction Buffer"),
size: (pixel_count * 4) as u64,
usage: wgpu::BufferUsages::STORAGE | wgpu::BufferUsages::COPY_SRC | wgpu::BufferUsages::COPY_DST,
mapped_at_creation: false,
});
let output_buffer = self.device.create_buffer(&wgpu::BufferDescriptor {
label: Some("Output Buffer"),
size: (pixel_count * 4) as u64,
usage: wgpu::BufferUsages::STORAGE | wgpu::BufferUsages::COPY_SRC,
mapped_at_creation: false,
});
let staging_buffer = self.device.create_buffer(&wgpu::BufferDescriptor {
label: Some("Staging Buffer"),
size: (pixel_count * 4) as u64,
usage: wgpu::BufferUsages::MAP_READ | wgpu::BufferUsages::COPY_DST,
mapped_at_creation: false,
});
let metadata = [width, height, iff_image.header.wavelet_levels as u32, 0];
let metadata_buffer = self.device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
label: Some("Metadata Buffer"),
contents: bytemuck::cast_slice(&metadata),
usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST,
});
let mut encoder = self.device.create_command_encoder(&wgpu::CommandEncoderDescriptor {
label: Some("Decode Command Encoder"),
});
self.dispatch_inverse_wavelet(
&mut encoder,
&layer1_buffer,
&metadata_buffer,
&reconstruction_buffer,
width,
height,
);
self.dispatch_noise_synthesis(
&mut encoder,
&layer2_buffer,
&metadata_buffer,
&reconstruction_buffer,
width,
height,
);
self.dispatch_warp_field(
&mut encoder,
&layer3_buffer,
&metadata_buffer,
&reconstruction_buffer,
width,
height,
);
self.dispatch_apply_residual(
&mut encoder,
&residual_buffer,
&metadata_buffer,
&reconstruction_buffer,
width,
height,
);
self.dispatch_composite(
&mut encoder,
&reconstruction_buffer,
&output_buffer,
width,
height,
);
encoder.copy_buffer_to_buffer(
&output_buffer,
0,
&staging_buffer,
0,
(pixel_count * 4) as u64,
);
self.queue.submit(Some(encoder.finish()));
let buffer_slice = staging_buffer.slice(..);
let (sender, receiver) = futures::channel::oneshot::channel();
buffer_slice.map_async(wgpu::MapMode::Read, move |result| {
sender.send(result).unwrap();
});
self.device.poll(wgpu::Maintain::Wait);
pollster::block_on(receiver)
.map_err(|_| IffError::Other("Failed to receive buffer map result".to_string()))?
.map_err(|e| IffError::Other(format!("Failed to map buffer: {:?}", e)))?;
let data = buffer_slice.get_mapped_range();
let result: Vec<u8> = data.to_vec();
drop(data);
staging_buffer.unmap();
let mut rgb_data = Vec::with_capacity(pixel_count * 3);
for chunk in result.chunks_exact(4) {
rgb_data.push(chunk[0]);
rgb_data.push(chunk[1]);
rgb_data.push(chunk[2]);
}
Ok(rgb_data)
}
fn create_layer1_buffer(&self, iff_image: &IffImage) -> Result<wgpu::Buffer> {
let data = bincode::serialize(&iff_image.layer1)?;
Ok(self.device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
label: Some("Layer 1 Buffer"),
contents: &data,
usage: wgpu::BufferUsages::STORAGE,
}))
}
fn create_layer2_buffer(&self, iff_image: &IffImage) -> Result<wgpu::Buffer> {
let data = bincode::serialize(&iff_image.layer2)?;
Ok(self.device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
label: Some("Layer 2 Buffer"),
contents: &data,
usage: wgpu::BufferUsages::STORAGE,
}))
}
fn create_layer3_buffer(&self, iff_image: &IffImage) -> Result<wgpu::Buffer> {
let data = bincode::serialize(&iff_image.layer3)?;
Ok(self.device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
label: Some("Layer 3 Buffer"),
contents: &data,
usage: wgpu::BufferUsages::STORAGE,
}))
}
fn create_residual_buffer(&self, iff_image: &IffImage) -> Result<wgpu::Buffer> {
let data = bincode::serialize(&iff_image.residual)?;
Ok(self.device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
label: Some("Residual Buffer"),
contents: &data,
usage: wgpu::BufferUsages::STORAGE,
}))
}
fn create_inverse_wavelet_pipeline(device: &wgpu::Device) -> Result<wgpu::ComputePipeline> {
let shader_source = include_str!("shaders/inverse_wavelet.wgsl");
let shader = device.create_shader_module(wgpu::ShaderModuleDescriptor {
label: Some("Inverse Wavelet Shader"),
source: wgpu::ShaderSource::Wgsl(shader_source.into()),
});
let bind_group_layout = device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
label: Some("Inverse Wavelet Bind Group Layout"),
entries: &[
wgpu::BindGroupLayoutEntry {
binding: 0,
visibility: wgpu::ShaderStages::COMPUTE,
ty: wgpu::BindingType::Buffer {
ty: wgpu::BufferBindingType::Storage { read_only: true },
has_dynamic_offset: false,
min_binding_size: None,
},
count: None,
},
wgpu::BindGroupLayoutEntry {
binding: 1,
visibility: wgpu::ShaderStages::COMPUTE,
ty: wgpu::BindingType::Buffer {
ty: wgpu::BufferBindingType::Uniform,
has_dynamic_offset: false,
min_binding_size: None,
},
count: None,
},
wgpu::BindGroupLayoutEntry {
binding: 2,
visibility: wgpu::ShaderStages::COMPUTE,
ty: wgpu::BindingType::Buffer {
ty: wgpu::BufferBindingType::Storage { read_only: false },
has_dynamic_offset: false,
min_binding_size: None,
},
count: None,
},
],
});
let pipeline_layout = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
label: Some("Inverse Wavelet Pipeline Layout"),
bind_group_layouts: &[&bind_group_layout],
push_constant_ranges: &[],
});
Ok(device.create_compute_pipeline(&wgpu::ComputePipelineDescriptor {
label: Some("Inverse Wavelet Pipeline"),
layout: Some(&pipeline_layout),
module: &shader,
entry_point: "main",
}))
}
fn create_noise_synthesis_pipeline(device: &wgpu::Device) -> Result<wgpu::ComputePipeline> {
let shader_source = include_str!("shaders/noise_synthesis.wgsl");
let shader = device.create_shader_module(wgpu::ShaderModuleDescriptor {
label: Some("Noise Synthesis Shader"),
source: wgpu::ShaderSource::Wgsl(shader_source.into()),
});
let bind_group_layout = device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
label: Some("Noise Synthesis Bind Group Layout"),
entries: &[
wgpu::BindGroupLayoutEntry {
binding: 0,
visibility: wgpu::ShaderStages::COMPUTE,
ty: wgpu::BindingType::Buffer {
ty: wgpu::BufferBindingType::Storage { read_only: true },
has_dynamic_offset: false,
min_binding_size: None,
},
count: None,
},
wgpu::BindGroupLayoutEntry {
binding: 1,
visibility: wgpu::ShaderStages::COMPUTE,
ty: wgpu::BindingType::Buffer {
ty: wgpu::BufferBindingType::Uniform,
has_dynamic_offset: false,
min_binding_size: None,
},
count: None,
},
wgpu::BindGroupLayoutEntry {
binding: 2,
visibility: wgpu::ShaderStages::COMPUTE,
ty: wgpu::BindingType::Buffer {
ty: wgpu::BufferBindingType::Storage { read_only: false },
has_dynamic_offset: false,
min_binding_size: None,
},
count: None,
},
],
});
let pipeline_layout = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
label: Some("Noise Synthesis Pipeline Layout"),
bind_group_layouts: &[&bind_group_layout],
push_constant_ranges: &[],
});
Ok(device.create_compute_pipeline(&wgpu::ComputePipelineDescriptor {
label: Some("Noise Synthesis Pipeline"),
layout: Some(&pipeline_layout),
module: &shader,
entry_point: "main",
}))
}
fn create_warp_field_pipeline(device: &wgpu::Device) -> Result<wgpu::ComputePipeline> {
let shader_source = include_str!("shaders/warp_field.wgsl");
let shader = device.create_shader_module(wgpu::ShaderModuleDescriptor {
label: Some("Warp Field Shader"),
source: wgpu::ShaderSource::Wgsl(shader_source.into()),
});
let bind_group_layout = device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
label: Some("Warp Field Bind Group Layout"),
entries: &[
wgpu::BindGroupLayoutEntry {
binding: 0,
visibility: wgpu::ShaderStages::COMPUTE,
ty: wgpu::BindingType::Buffer {
ty: wgpu::BufferBindingType::Storage { read_only: true },
has_dynamic_offset: false,
min_binding_size: None,
},
count: None,
},
wgpu::BindGroupLayoutEntry {
binding: 1,
visibility: wgpu::ShaderStages::COMPUTE,
ty: wgpu::BindingType::Buffer {
ty: wgpu::BufferBindingType::Uniform,
has_dynamic_offset: false,
min_binding_size: None,
},
count: None,
},
wgpu::BindGroupLayoutEntry {
binding: 2,
visibility: wgpu::ShaderStages::COMPUTE,
ty: wgpu::BindingType::Buffer {
ty: wgpu::BufferBindingType::Storage { read_only: false },
has_dynamic_offset: false,
min_binding_size: None,
},
count: None,
},
],
});
let pipeline_layout = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
label: Some("Warp Field Pipeline Layout"),
bind_group_layouts: &[&bind_group_layout],
push_constant_ranges: &[],
});
Ok(device.create_compute_pipeline(&wgpu::ComputePipelineDescriptor {
label: Some("Warp Field Pipeline"),
layout: Some(&pipeline_layout),
module: &shader,
entry_point: "main",
}))
}
fn create_apply_warp_pipeline(device: &wgpu::Device) -> Result<wgpu::ComputePipeline> {
let shader_source = include_str!("shaders/apply_warp.wgsl");
let shader = device.create_shader_module(wgpu::ShaderModuleDescriptor {
label: Some("Apply Warp Shader"),
source: wgpu::ShaderSource::Wgsl(shader_source.into()),
});
let bind_group_layout = device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
label: Some("Apply Warp Bind Group Layout"),
entries: &[
wgpu::BindGroupLayoutEntry {
binding: 0,
visibility: wgpu::ShaderStages::COMPUTE,
ty: wgpu::BindingType::Buffer {
ty: wgpu::BufferBindingType::Storage { read_only: true },
has_dynamic_offset: false,
min_binding_size: None,
},
count: None,
},
wgpu::BindGroupLayoutEntry {
binding: 1,
visibility: wgpu::ShaderStages::COMPUTE,
ty: wgpu::BindingType::Buffer {
ty: wgpu::BufferBindingType::Uniform,
has_dynamic_offset: false,
min_binding_size: None,
},
count: None,
},
wgpu::BindGroupLayoutEntry {
binding: 2,
visibility: wgpu::ShaderStages::COMPUTE,
ty: wgpu::BindingType::Buffer {
ty: wgpu::BufferBindingType::Storage { read_only: false },
has_dynamic_offset: false,
min_binding_size: None,
},
count: None,
},
],
});
let pipeline_layout = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
label: Some("Apply Warp Pipeline Layout"),
bind_group_layouts: &[&bind_group_layout],
push_constant_ranges: &[],
});
Ok(device.create_compute_pipeline(&wgpu::ComputePipelineDescriptor {
label: Some("Apply Warp Pipeline"),
layout: Some(&pipeline_layout),
module: &shader,
entry_point: "main",
}))
}
fn create_apply_residual_pipeline(device: &wgpu::Device) -> Result<wgpu::ComputePipeline> {
let shader_source = include_str!("shaders/apply_residual.wgsl");
let shader = device.create_shader_module(wgpu::ShaderModuleDescriptor {
label: Some("Apply Residual Shader"),
source: wgpu::ShaderSource::Wgsl(shader_source.into()),
});
let bind_group_layout = device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
label: Some("Apply Residual Bind Group Layout"),
entries: &[
wgpu::BindGroupLayoutEntry {
binding: 0,
visibility: wgpu::ShaderStages::COMPUTE,
ty: wgpu::BindingType::Buffer {
ty: wgpu::BufferBindingType::Storage { read_only: true },
has_dynamic_offset: false,
min_binding_size: None,
},
count: None,
},
wgpu::BindGroupLayoutEntry {
binding: 1,
visibility: wgpu::ShaderStages::COMPUTE,
ty: wgpu::BindingType::Buffer {
ty: wgpu::BufferBindingType::Uniform,
has_dynamic_offset: false,
min_binding_size: None,
},
count: None,
},
wgpu::BindGroupLayoutEntry {
binding: 2,
visibility: wgpu::ShaderStages::COMPUTE,
ty: wgpu::BindingType::Buffer {
ty: wgpu::BufferBindingType::Storage { read_only: false },
has_dynamic_offset: false,
min_binding_size: None,
},
count: None,
},
],
});
let pipeline_layout = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
label: Some("Apply Residual Pipeline Layout"),
bind_group_layouts: &[&bind_group_layout],
push_constant_ranges: &[],
});
Ok(device.create_compute_pipeline(&wgpu::ComputePipelineDescriptor {
label: Some("Apply Residual Pipeline"),
layout: Some(&pipeline_layout),
module: &shader,
entry_point: "main",
}))
}
fn create_composite_pipeline(device: &wgpu::Device) -> Result<wgpu::ComputePipeline> {
let shader_source = include_str!("shaders/composite.wgsl");
let shader = device.create_shader_module(wgpu::ShaderModuleDescriptor {
label: Some("Composite Shader"),
source: wgpu::ShaderSource::Wgsl(shader_source.into()),
});
let bind_group_layout = device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
label: Some("Composite Bind Group Layout"),
entries: &[
wgpu::BindGroupLayoutEntry {
binding: 0,
visibility: wgpu::ShaderStages::COMPUTE,
ty: wgpu::BindingType::Buffer {
ty: wgpu::BufferBindingType::Storage { read_only: true },
has_dynamic_offset: false,
min_binding_size: None,
},
count: None,
},
wgpu::BindGroupLayoutEntry {
binding: 1,
visibility: wgpu::ShaderStages::COMPUTE,
ty: wgpu::BindingType::Buffer {
ty: wgpu::BufferBindingType::Storage { read_only: false },
has_dynamic_offset: false,
min_binding_size: None,
},
count: None,
},
],
});
let pipeline_layout = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
label: Some("Composite Pipeline Layout"),
bind_group_layouts: &[&bind_group_layout],
push_constant_ranges: &[],
});
Ok(device.create_compute_pipeline(&wgpu::ComputePipelineDescriptor {
label: Some("Composite Pipeline"),
layout: Some(&pipeline_layout),
module: &shader,
entry_point: "main",
}))
}
fn dispatch_inverse_wavelet(
&self,
encoder: &mut wgpu::CommandEncoder,
layer1_buffer: &wgpu::Buffer,
metadata_buffer: &wgpu::Buffer,
output_buffer: &wgpu::Buffer,
width: u32,
height: u32,
) {
let bind_group = self.device.create_bind_group(&wgpu::BindGroupDescriptor {
label: Some("Inverse Wavelet Bind Group"),
layout: &self.inverse_wavelet_pipeline.get_bind_group_layout(0),
entries: &[
wgpu::BindGroupEntry {
binding: 0,
resource: layer1_buffer.as_entire_binding(),
},
wgpu::BindGroupEntry {
binding: 1,
resource: metadata_buffer.as_entire_binding(),
},
wgpu::BindGroupEntry {
binding: 2,
resource: output_buffer.as_entire_binding(),
},
],
});
let mut compute_pass = encoder.begin_compute_pass(&wgpu::ComputePassDescriptor {
label: Some("Inverse Wavelet Pass"),
timestamp_writes: None,
});
compute_pass.set_pipeline(&self.inverse_wavelet_pipeline);
compute_pass.set_bind_group(0, &bind_group, &[]);
compute_pass.dispatch_workgroups((width + 7) / 8, (height + 7) / 8, 1);
}
fn dispatch_noise_synthesis(
&self,
encoder: &mut wgpu::CommandEncoder,
layer2_buffer: &wgpu::Buffer,
metadata_buffer: &wgpu::Buffer,
output_buffer: &wgpu::Buffer,
width: u32,
height: u32,
) {
let bind_group = self.device.create_bind_group(&wgpu::BindGroupDescriptor {
label: Some("Noise Synthesis Bind Group"),
layout: &self.noise_synthesis_pipeline.get_bind_group_layout(0),
entries: &[
wgpu::BindGroupEntry {
binding: 0,
resource: layer2_buffer.as_entire_binding(),
},
wgpu::BindGroupEntry {
binding: 1,
resource: metadata_buffer.as_entire_binding(),
},
wgpu::BindGroupEntry {
binding: 2,
resource: output_buffer.as_entire_binding(),
},
],
});
let mut compute_pass = encoder.begin_compute_pass(&wgpu::ComputePassDescriptor {
label: Some("Noise Synthesis Pass"),
timestamp_writes: None,
});
compute_pass.set_pipeline(&self.noise_synthesis_pipeline);
compute_pass.set_bind_group(0, &bind_group, &[]);
compute_pass.dispatch_workgroups((width + 7) / 8, (height + 7) / 8, 1);
}
fn dispatch_warp_field(
&self,
encoder: &mut wgpu::CommandEncoder,
layer3_buffer: &wgpu::Buffer,
metadata_buffer: &wgpu::Buffer,
output_buffer: &wgpu::Buffer,
width: u32,
height: u32,
) {
let bind_group = self.device.create_bind_group(&wgpu::BindGroupDescriptor {
label: Some("Warp Field Bind Group"),
layout: &self.warp_field_pipeline.get_bind_group_layout(0),
entries: &[
wgpu::BindGroupEntry {
binding: 0,
resource: layer3_buffer.as_entire_binding(),
},
wgpu::BindGroupEntry {
binding: 1,
resource: metadata_buffer.as_entire_binding(),
},
wgpu::BindGroupEntry {
binding: 2,
resource: output_buffer.as_entire_binding(),
},
],
});
let mut compute_pass = encoder.begin_compute_pass(&wgpu::ComputePassDescriptor {
label: Some("Warp Field Pass"),
timestamp_writes: None,
});
compute_pass.set_pipeline(&self.warp_field_pipeline);
compute_pass.set_bind_group(0, &bind_group, &[]);
compute_pass.dispatch_workgroups((width + 7) / 8, (height + 7) / 8, 1);
}
fn dispatch_apply_residual(
&self,
encoder: &mut wgpu::CommandEncoder,
residual_buffer: &wgpu::Buffer,
metadata_buffer: &wgpu::Buffer,
output_buffer: &wgpu::Buffer,
width: u32,
height: u32,
) {
let bind_group = self.device.create_bind_group(&wgpu::BindGroupDescriptor {
label: Some("Apply Residual Bind Group"),
layout: &self.apply_residual_pipeline.get_bind_group_layout(0),
entries: &[
wgpu::BindGroupEntry {
binding: 0,
resource: residual_buffer.as_entire_binding(),
},
wgpu::BindGroupEntry {
binding: 1,
resource: metadata_buffer.as_entire_binding(),
},
wgpu::BindGroupEntry {
binding: 2,
resource: output_buffer.as_entire_binding(),
},
],
});
let mut compute_pass = encoder.begin_compute_pass(&wgpu::ComputePassDescriptor {
label: Some("Apply Residual Pass"),
timestamp_writes: None,
});
compute_pass.set_pipeline(&self.apply_residual_pipeline);
compute_pass.set_bind_group(0, &bind_group, &[]);
compute_pass.dispatch_workgroups((width + 7) / 8, (height + 7) / 8, 1);
}
fn dispatch_composite(
&self,
encoder: &mut wgpu::CommandEncoder,
input_buffer: &wgpu::Buffer,
output_buffer: &wgpu::Buffer,
width: u32,
height: u32,
) {
let bind_group = self.device.create_bind_group(&wgpu::BindGroupDescriptor {
label: Some("Composite Bind Group"),
layout: &self.composite_pipeline.get_bind_group_layout(0),
entries: &[
wgpu::BindGroupEntry {
binding: 0,
resource: input_buffer.as_entire_binding(),
},
wgpu::BindGroupEntry {
binding: 1,
resource: output_buffer.as_entire_binding(),
},
],
});
let mut compute_pass = encoder.begin_compute_pass(&wgpu::ComputePassDescriptor {
label: Some("Composite Pass"),
timestamp_writes: None,
});
compute_pass.set_pipeline(&self.composite_pipeline);
compute_pass.set_bind_group(0, &bind_group, &[]);
compute_pass.dispatch_workgroups((width + 7) / 8, (height + 7) / 8, 1);
}
}
#[cfg(not(feature = "gpu"))]
pub struct GpuDecoder {}
#[cfg(not(feature = "gpu"))]
impl GpuDecoder {
pub fn new() -> Result<Self> {
Err(IffError::Other("GPU feature not enabled".to_string()))
}
pub fn decode(&self, _iff_image: &IffImage) -> Result<Vec<u8>> {
Err(IffError::Other("GPU feature not enabled".to_string()))
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_gpu_decoder_stub() {
#[cfg(not(feature = "gpu"))]
{
let result = GpuDecoder::new();
assert!(result.is_err());
}
}
}