use std::sync::Mutex;
#[repr(C)]
#[derive(Clone, Copy)]
pub struct Vert {
pub pos: [f32; 2],
pub color: [f32; 3],
}
struct Gpu {
device: wgpu::Device,
queue: wgpu::Queue,
pipeline: wgpu::RenderPipeline,
uniform: wgpu::Buffer,
bind_group: wgpu::BindGroup,
}
static GPU: Mutex<Option<Gpu>> = Mutex::new(None);
static INIT_FAILED: Mutex<bool> = Mutex::new(false);
const WGSL: &str = r#"
struct Uniforms { size: vec2<f32> };
@group(0) @binding(0) var<uniform> u: Uniforms;
struct VOut {
@builtin(position) pos: vec4<f32>,
@location(0) color: vec3<f32>,
};
@vertex
fn vs(@location(0) screen: vec2<f32>, @location(1) color: vec3<f32>) -> VOut {
var o: VOut;
// screen pixels -> NDC; flip Y so framebuffer row 0 is the top.
let ndc = (screen / u.size) * 2.0 - vec2<f32>(1.0, 1.0);
o.pos = vec4<f32>(ndc.x, -ndc.y, 0.0, 1.0);
o.color = color;
return o;
}
@fragment
fn fs(i: VOut) -> @location(0) vec4<f32> {
return vec4<f32>(i.color, 1.0);
}
"#;
fn try_init() -> Option<Gpu> {
let instance = wgpu::Instance::new(wgpu::InstanceDescriptor::default());
let adapter = pollster::block_on(instance.request_adapter(&wgpu::RequestAdapterOptions {
power_preference: wgpu::PowerPreference::HighPerformance,
force_fallback_adapter: false,
compatible_surface: None,
}))?;
let (device, queue) = pollster::block_on(adapter.request_device(
&wgpu::DeviceDescriptor {
label: Some("ling-gpu-raster"),
required_features: wgpu::Features::empty(),
required_limits: wgpu::Limits::downlevel_defaults(),
memory_hints: wgpu::MemoryHints::Performance,
},
None,
))
.ok()?;
let shader = device.create_shader_module(wgpu::ShaderModuleDescriptor {
label: Some("ling-raster-shader"),
source: wgpu::ShaderSource::Wgsl(WGSL.into()),
});
let uniform = device.create_buffer(&wgpu::BufferDescriptor {
label: Some("ling-uniform"),
size: 16, usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST,
mapped_at_creation: false,
});
let bgl = device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
label: Some("ling-bgl"),
entries: &[wgpu::BindGroupLayoutEntry {
binding: 0,
visibility: wgpu::ShaderStages::VERTEX,
ty: wgpu::BindingType::Buffer {
ty: wgpu::BufferBindingType::Uniform,
has_dynamic_offset: false,
min_binding_size: None,
},
count: None,
}],
});
let bind_group = device.create_bind_group(&wgpu::BindGroupDescriptor {
label: Some("ling-bg"),
layout: &bgl,
entries: &[wgpu::BindGroupEntry {
binding: 0,
resource: uniform.as_entire_binding(),
}],
});
let pl = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
label: Some("ling-pl"),
bind_group_layouts: &[&bgl],
push_constant_ranges: &[],
});
let pipeline = device.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
label: Some("ling-pipeline"),
layout: Some(&pl),
vertex: wgpu::VertexState {
module: &shader,
entry_point: "vs",
compilation_options: Default::default(),
buffers: &[wgpu::VertexBufferLayout {
array_stride: 20, step_mode: wgpu::VertexStepMode::Vertex,
attributes: &[
wgpu::VertexAttribute {
format: wgpu::VertexFormat::Float32x2,
offset: 0,
shader_location: 0,
},
wgpu::VertexAttribute {
format: wgpu::VertexFormat::Float32x3,
offset: 8,
shader_location: 1,
},
],
}],
},
fragment: Some(wgpu::FragmentState {
module: &shader,
entry_point: "fs",
compilation_options: Default::default(),
targets: &[Some(wgpu::ColorTargetState {
format: wgpu::TextureFormat::Rgba8Unorm,
blend: Some(wgpu::BlendState::ALPHA_BLENDING),
write_mask: wgpu::ColorWrites::ALL,
})],
}),
primitive: wgpu::PrimitiveState::default(),
depth_stencil: None,
multisample: wgpu::MultisampleState::default(),
multiview: None,
cache: None,
});
Some(Gpu {
device,
queue,
pipeline,
uniform,
bind_group,
})
}
pub fn raster(
verts: &[Vert],
width: usize,
height: usize,
clear: [f32; 3],
out: &mut [u32],
) -> bool {
if width == 0 || height == 0 {
return false;
}
if *INIT_FAILED.lock().unwrap() {
return false;
}
let mut guard = GPU.lock().unwrap();
if guard.is_none() {
match try_init() {
Some(g) => *guard = Some(g),
None => {
*INIT_FAILED.lock().unwrap() = true;
return false;
}
}
}
let g = guard.as_ref().unwrap();
let size = [width as f32, height as f32, 0.0f32, 0.0f32];
g.queue.write_buffer(&g.uniform, 0, f32_bytes(&size));
let mut vbytes: Vec<u8> = Vec::with_capacity(verts.len() * 20);
for v in verts {
for f in [v.pos[0], v.pos[1], v.color[0], v.color[1], v.color[2]] {
vbytes.extend_from_slice(&f.to_le_bytes());
}
}
let vbuf = g.device.create_buffer(&wgpu::BufferDescriptor {
label: Some("ling-verts"),
size: vbytes.len().max(20) as u64,
usage: wgpu::BufferUsages::VERTEX | wgpu::BufferUsages::COPY_DST,
mapped_at_creation: false,
});
if !vbytes.is_empty() {
g.queue.write_buffer(&vbuf, 0, &vbytes);
}
let tex = g.device.create_texture(&wgpu::TextureDescriptor {
label: Some("ling-target"),
size: wgpu::Extent3d {
width: width as u32,
height: height as u32,
depth_or_array_layers: 1,
},
mip_level_count: 1,
sample_count: 1,
dimension: wgpu::TextureDimension::D2,
format: wgpu::TextureFormat::Rgba8Unorm,
usage: wgpu::TextureUsages::RENDER_ATTACHMENT | wgpu::TextureUsages::COPY_SRC,
view_formats: &[],
});
let view = tex.create_view(&wgpu::TextureViewDescriptor::default());
let unpadded = width * 4;
let align = 256usize;
let padded = unpadded.div_ceil(align) * align;
let rb = g.device.create_buffer(&wgpu::BufferDescriptor {
label: Some("ling-readback"),
size: (padded * height) as u64,
usage: wgpu::BufferUsages::COPY_DST | wgpu::BufferUsages::MAP_READ,
mapped_at_creation: false,
});
let mut enc = g
.device
.create_command_encoder(&wgpu::CommandEncoderDescriptor {
label: Some("ling-enc"),
});
{
let mut rp = enc.begin_render_pass(&wgpu::RenderPassDescriptor {
label: Some("ling-rp"),
color_attachments: &[Some(wgpu::RenderPassColorAttachment {
view: &view,
resolve_target: None,
ops: wgpu::Operations {
load: wgpu::LoadOp::Clear(wgpu::Color {
r: clear[0] as f64,
g: clear[1] as f64,
b: clear[2] as f64,
a: 1.0,
}),
store: wgpu::StoreOp::Store,
},
})],
depth_stencil_attachment: None,
timestamp_writes: None,
occlusion_query_set: None,
});
if !verts.is_empty() {
rp.set_pipeline(&g.pipeline);
rp.set_bind_group(0, &g.bind_group, &[]);
rp.set_vertex_buffer(0, vbuf.slice(..));
rp.draw(0..verts.len() as u32, 0..1);
}
}
enc.copy_texture_to_buffer(
wgpu::ImageCopyTexture {
texture: &tex,
mip_level: 0,
origin: wgpu::Origin3d::ZERO,
aspect: wgpu::TextureAspect::All,
},
wgpu::ImageCopyBuffer {
buffer: &rb,
layout: wgpu::ImageDataLayout {
offset: 0,
bytes_per_row: Some(padded as u32),
rows_per_image: Some(height as u32),
},
},
wgpu::Extent3d {
width: width as u32,
height: height as u32,
depth_or_array_layers: 1,
},
);
g.queue.submit(Some(enc.finish()));
let slice = rb.slice(..);
let (tx, rx) = std::sync::mpsc::channel();
slice.map_async(wgpu::MapMode::Read, move |r| {
let _ = tx.send(r);
});
let _ = g.device.poll(wgpu::Maintain::Wait);
if !matches!(rx.recv(), Ok(Ok(()))) {
return false;
}
{
let data = slice.get_mapped_range();
let n = (width * height).min(out.len());
for y in 0..height {
let base = y * padded;
for x in 0..width {
let i = y * width + x;
if i >= n {
break;
}
let p = base + x * 4;
let r = data[p] as u32;
let gg = data[p + 1] as u32;
let b = data[p + 2] as u32;
out[i] = (r << 16) | (gg << 8) | b;
}
}
}
rb.unmap();
true
}
fn f32_bytes(a: &[f32]) -> &[u8] {
unsafe { std::slice::from_raw_parts(a.as_ptr() as *const u8, std::mem::size_of_val(a)) }
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn gpu_triangle_renders() {
let (w, h) = (64usize, 64usize);
let verts = vec![
Vert { pos: [6.0, 58.0], color: [1.0, 0.0, 0.0] },
Vert { pos: [58.0, 58.0], color: [1.0, 0.0, 0.0] },
Vert { pos: [32.0, 6.0], color: [1.0, 0.0, 0.0] },
];
let mut out = vec![0u32; w * h];
if !raster(&verts, w, h, [0.0, 0.0, 0.0], &mut out) {
eprintln!("no GPU adapter available — skipping GPU raster test");
return;
}
let center = out[(h / 2) * w + w / 2];
let cr = (center >> 16) & 0xFF;
assert!(cr > 200, "centre pixel should be red, got {center:06X}");
let corner = out[2];
assert!(
(corner & 0x00FF_FFFF) < 0x0010_1010,
"corner should be ~black, got {corner:06X}"
);
}
}