use std::collections::HashMap;
use std::num::NonZeroU64;
use noesis_runtime::render_device::types::{
Batch, DeviceCaps, SIZE_FOR_FORMAT, SamplerState, Shader, TextureFormat, Tile,
};
use noesis_runtime::render_device::{
RenderDevice, RenderTargetBinding, RenderTargetDesc, RenderTargetHandle, TextureBinding,
TextureDesc, TextureHandle, TextureRect,
};
use crate::render_device::pipeline::{PipelineCache, PipelineKey, STENCIL_FORMAT};
const DYNAMIC_VB_SIZE: u64 = 512 * 1024;
const DYNAMIC_IB_SIZE: u64 = 128 * 1024;
const VS_UNIFORM_SIZE: u64 = 80;
const VS_GLYPH_SIZE_OFFSET: usize = 64;
const PS_UNIFORM0_SIZE: u64 = 32;
const PS_UNIFORM1_SIZE: u64 = 32;
const UNIFORM_RING_SLOTS: u32 = 1024;
const RT_COLOR_FORMAT: wgpu::TextureFormat = wgpu::TextureFormat::Rgba8Unorm;
const RT_STENCIL_FORMAT: wgpu::TextureFormat = STENCIL_FORMAT;
#[derive(Copy, Clone, Debug, PartialEq, Eq)]
enum FramePhase {
Idle,
Offscreen,
Onscreen,
}
pub struct WgpuRenderDevice {
device: wgpu::Device,
queue: wgpu::Queue,
vertex_buffer: wgpu::Buffer,
vertex_staging: Vec<u8>,
vertex_mapped_bytes: Option<u32>,
index_buffer: wgpu::Buffer,
index_staging: Vec<u8>,
index_mapped_bytes: Option<u32>,
vs_ring: UniformRing,
vs_uniform_bind_group: wgpu::BindGroup,
ps_ring: UniformRing,
ps1_ring: UniformRing,
ps_uniform_bind_group: wgpu::BindGroup,
pattern_bind_group_layout: wgpu::BindGroupLayout,
dummy_pattern_bg: wgpu::BindGroup,
samplers: HashMap<SamplerState, wgpu::Sampler>,
pattern_bind_groups: HashMap<(TextureHandle, SamplerState), wgpu::BindGroup>,
image_bind_group_layout: wgpu::BindGroupLayout,
dummy_image_bg: wgpu::BindGroup,
image_bind_groups: HashMap<ImageBindGroupKey, wgpu::BindGroup>,
#[allow(dead_code)] dummy_texture: wgpu::Texture,
dummy_view: wgpu::TextureView,
dummy_sampler: wgpu::Sampler,
pipelines: PipelineCache,
textures: HashMap<TextureHandle, GpuTexture>,
render_targets: HashMap<RenderTargetHandle, GpuRenderTarget>,
target_view: Option<wgpu::TextureView>,
onscreen_stencil: Option<GpuStencil>,
onscreen_stencil_cleared: bool,
current_rt_stencil_cleared: bool,
phase: FramePhase,
encoder: Option<wgpu::CommandEncoder>,
current_rt: Option<RenderTargetHandle>,
current_tile: Option<Tile>,
forced_pattern: Option<(TextureHandle, SamplerState)>,
forced_image: Option<(TextureHandle, SamplerState)>,
forced_shadow: Option<(TextureHandle, SamplerState)>,
next_handle: u64,
}
type ImageBindGroupKey = (
TextureHandle,
SamplerState,
Option<(TextureHandle, SamplerState)>,
);
#[allow(dead_code)] struct GpuTexture {
texture: wgpu::Texture,
view: wgpu::TextureView,
noesis_format: TextureFormat,
width: u32,
height: u32,
num_levels: u32,
}
struct GpuStencil {
#[allow(dead_code)] texture: wgpu::Texture,
view: wgpu::TextureView,
width: u32,
height: u32,
}
struct GpuRenderTarget {
color_view: wgpu::TextureView,
#[allow(dead_code)] resolve_handle: TextureHandle,
stencil: Option<(wgpu::Texture, wgpu::TextureView)>,
width: u32,
height: u32,
}
impl WgpuRenderDevice {
#[must_use]
#[allow(clippy::too_many_lines)] pub fn new(device: wgpu::Device, queue: wgpu::Queue) -> Self {
let vertex_buffer = device.create_buffer(&wgpu::BufferDescriptor {
label: Some("noesis_runtime vertex stream"),
size: DYNAMIC_VB_SIZE,
usage: wgpu::BufferUsages::VERTEX | wgpu::BufferUsages::COPY_DST,
mapped_at_creation: false,
});
let index_buffer = device.create_buffer(&wgpu::BufferDescriptor {
label: Some("noesis_runtime index stream"),
size: DYNAMIC_IB_SIZE,
usage: wgpu::BufferUsages::INDEX | wgpu::BufferUsages::COPY_DST,
mapped_at_creation: false,
});
let uniform_alignment = u64::from(device.limits().min_uniform_buffer_offset_alignment);
let vs_ring = UniformRing::new(
&device,
"noesis_runtime vs_uniforms ring (mat4 projection)",
VS_UNIFORM_SIZE,
UNIFORM_RING_SLOTS,
uniform_alignment,
);
let ps_ring = UniformRing::new(
&device,
"noesis_runtime ps_uniforms0 ring (cbuffer0_ps[8])",
PS_UNIFORM0_SIZE,
UNIFORM_RING_SLOTS,
uniform_alignment,
);
let ps1_ring = UniformRing::new(
&device,
"noesis_runtime ps_uniforms1 ring (cbuffer1_ps[8])",
PS_UNIFORM1_SIZE,
UNIFORM_RING_SLOTS,
uniform_alignment,
);
let vs_uniform_bind_group_layout =
device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
label: Some("noesis_runtime vs_uniforms layout"),
entries: &[wgpu::BindGroupLayoutEntry {
binding: 0,
visibility: wgpu::ShaderStages::VERTEX,
ty: wgpu::BindingType::Buffer {
ty: wgpu::BufferBindingType::Uniform,
has_dynamic_offset: true,
min_binding_size: NonZeroU64::new(VS_UNIFORM_SIZE),
},
count: None,
}],
});
let ps_uniform_bind_group_layout =
device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
label: Some("noesis_runtime ps_uniforms layout"),
entries: &[
wgpu::BindGroupLayoutEntry {
binding: 0,
visibility: wgpu::ShaderStages::FRAGMENT,
ty: wgpu::BindingType::Buffer {
ty: wgpu::BufferBindingType::Uniform,
has_dynamic_offset: true,
min_binding_size: NonZeroU64::new(PS_UNIFORM0_SIZE),
},
count: None,
},
wgpu::BindGroupLayoutEntry {
binding: 1,
visibility: wgpu::ShaderStages::FRAGMENT,
ty: wgpu::BindingType::Buffer {
ty: wgpu::BufferBindingType::Uniform,
has_dynamic_offset: true,
min_binding_size: NonZeroU64::new(PS_UNIFORM1_SIZE),
},
count: None,
},
],
});
let vs_uniform_bind_group = device.create_bind_group(&wgpu::BindGroupDescriptor {
label: Some("noesis_runtime vs_uniforms"),
layout: &vs_uniform_bind_group_layout,
entries: &[wgpu::BindGroupEntry {
binding: 0,
resource: wgpu::BindingResource::Buffer(wgpu::BufferBinding {
buffer: vs_ring.buffer(),
offset: 0,
size: NonZeroU64::new(VS_UNIFORM_SIZE),
}),
}],
});
let ps_uniform_bind_group = device.create_bind_group(&wgpu::BindGroupDescriptor {
label: Some("noesis_runtime ps_uniforms"),
layout: &ps_uniform_bind_group_layout,
entries: &[
wgpu::BindGroupEntry {
binding: 0,
resource: wgpu::BindingResource::Buffer(wgpu::BufferBinding {
buffer: ps_ring.buffer(),
offset: 0,
size: NonZeroU64::new(PS_UNIFORM0_SIZE),
}),
},
wgpu::BindGroupEntry {
binding: 1,
resource: wgpu::BindingResource::Buffer(wgpu::BufferBinding {
buffer: ps1_ring.buffer(),
offset: 0,
size: NonZeroU64::new(PS_UNIFORM1_SIZE),
}),
},
],
});
let pattern_bind_group_layout =
device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
label: Some("noesis_runtime pattern layout"),
entries: &[
wgpu::BindGroupLayoutEntry {
binding: 0,
visibility: wgpu::ShaderStages::FRAGMENT,
ty: wgpu::BindingType::Texture {
sample_type: wgpu::TextureSampleType::Float { filterable: true },
view_dimension: wgpu::TextureViewDimension::D2,
multisampled: false,
},
count: None,
},
wgpu::BindGroupLayoutEntry {
binding: 1,
visibility: wgpu::ShaderStages::FRAGMENT,
ty: wgpu::BindingType::Sampler(wgpu::SamplerBindingType::Filtering),
count: None,
},
],
});
let texture_entry = |binding| wgpu::BindGroupLayoutEntry {
binding,
visibility: wgpu::ShaderStages::FRAGMENT,
ty: wgpu::BindingType::Texture {
sample_type: wgpu::TextureSampleType::Float { filterable: true },
view_dimension: wgpu::TextureViewDimension::D2,
multisampled: false,
},
count: None,
};
let sampler_entry = |binding| wgpu::BindGroupLayoutEntry {
binding,
visibility: wgpu::ShaderStages::FRAGMENT,
ty: wgpu::BindingType::Sampler(wgpu::SamplerBindingType::Filtering),
count: None,
};
let image_bind_group_layout =
device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
label: Some("noesis_runtime image+shadow layout"),
entries: &[
texture_entry(0),
sampler_entry(1),
texture_entry(2),
sampler_entry(3),
],
});
let dummy_texture = device.create_texture(&wgpu::TextureDescriptor {
label: Some("noesis_runtime dummy pattern"),
size: wgpu::Extent3d {
width: 1,
height: 1,
depth_or_array_layers: 1,
},
mip_level_count: 1,
sample_count: 1,
dimension: wgpu::TextureDimension::D2,
format: wgpu::TextureFormat::Rgba8Unorm,
usage: wgpu::TextureUsages::TEXTURE_BINDING | wgpu::TextureUsages::COPY_DST,
view_formats: &[],
});
queue.write_texture(
wgpu::TexelCopyTextureInfo {
texture: &dummy_texture,
mip_level: 0,
origin: wgpu::Origin3d::ZERO,
aspect: wgpu::TextureAspect::All,
},
&[0xFF_u8; 4],
wgpu::TexelCopyBufferLayout {
offset: 0,
bytes_per_row: Some(4),
rows_per_image: Some(1),
},
wgpu::Extent3d {
width: 1,
height: 1,
depth_or_array_layers: 1,
},
);
let dummy_view = dummy_texture.create_view(&wgpu::TextureViewDescriptor::default());
let dummy_sampler = device.create_sampler(&wgpu::SamplerDescriptor {
label: Some("noesis_runtime dummy sampler"),
..Default::default()
});
let dummy_pattern_bg = device.create_bind_group(&wgpu::BindGroupDescriptor {
label: Some("noesis_runtime dummy pattern bg"),
layout: &pattern_bind_group_layout,
entries: &[
wgpu::BindGroupEntry {
binding: 0,
resource: wgpu::BindingResource::TextureView(&dummy_view),
},
wgpu::BindGroupEntry {
binding: 1,
resource: wgpu::BindingResource::Sampler(&dummy_sampler),
},
],
});
let dummy_image_bg = device.create_bind_group(&wgpu::BindGroupDescriptor {
label: Some("noesis_runtime dummy image bg"),
layout: &image_bind_group_layout,
entries: &[
wgpu::BindGroupEntry {
binding: 0,
resource: wgpu::BindingResource::TextureView(&dummy_view),
},
wgpu::BindGroupEntry {
binding: 1,
resource: wgpu::BindingResource::Sampler(&dummy_sampler),
},
wgpu::BindGroupEntry {
binding: 2,
resource: wgpu::BindingResource::TextureView(&dummy_view),
},
wgpu::BindGroupEntry {
binding: 3,
resource: wgpu::BindingResource::Sampler(&dummy_sampler),
},
],
});
let pipeline_layout = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
label: Some("noesis_runtime pipeline layout"),
bind_group_layouts: &[
&vs_uniform_bind_group_layout,
&ps_uniform_bind_group_layout,
&pattern_bind_group_layout,
&image_bind_group_layout,
],
push_constant_ranges: &[],
});
let pipelines = PipelineCache::new(device.clone(), pipeline_layout, RT_COLOR_FORMAT);
Self {
device,
queue,
vertex_buffer,
vertex_staging: vec![0; DYNAMIC_VB_SIZE as usize],
vertex_mapped_bytes: None,
index_buffer,
index_staging: vec![0; DYNAMIC_IB_SIZE as usize],
index_mapped_bytes: None,
vs_ring,
vs_uniform_bind_group,
ps_ring,
ps1_ring,
ps_uniform_bind_group,
pattern_bind_group_layout,
dummy_pattern_bg,
samplers: HashMap::new(),
pattern_bind_groups: HashMap::new(),
image_bind_group_layout,
dummy_image_bg,
image_bind_groups: HashMap::new(),
dummy_texture,
dummy_view,
dummy_sampler,
pipelines,
textures: HashMap::new(),
render_targets: HashMap::new(),
target_view: None,
onscreen_stencil: None,
onscreen_stencil_cleared: false,
current_rt_stencil_cleared: false,
phase: FramePhase::Idle,
encoder: None,
current_rt: None,
current_tile: None,
forced_pattern: None,
forced_image: None,
forced_shadow: None,
next_handle: 1,
}
}
pub fn test_set_forced_pattern(&mut self, forced: Option<(TextureHandle, SamplerState)>) {
self.forced_pattern = forced;
}
pub fn test_set_forced_image(&mut self, forced: Option<(TextureHandle, SamplerState)>) {
self.forced_image = forced;
}
pub fn test_set_forced_shadow(&mut self, forced: Option<(TextureHandle, SamplerState)>) {
self.forced_shadow = forced;
}
fn pattern_bind_group_for(
&mut self,
handle: TextureHandle,
state: SamplerState,
) -> &wgpu::BindGroup {
if self.pattern_bind_groups.contains_key(&(handle, state)) {
return self
.pattern_bind_groups
.get(&(handle, state))
.expect("just checked contains_key");
}
let view = self
.textures
.get(&handle)
.map(|t| &t.view)
.expect("pattern_bind_group_for: unknown TextureHandle");
let sampler = self
.samplers
.entry(state)
.or_insert_with(|| build_sampler(&self.device, state));
let bg = self.device.create_bind_group(&wgpu::BindGroupDescriptor {
label: Some("noesis_runtime pattern bg"),
layout: &self.pattern_bind_group_layout,
entries: &[
wgpu::BindGroupEntry {
binding: 0,
resource: wgpu::BindingResource::TextureView(view),
},
wgpu::BindGroupEntry {
binding: 1,
resource: wgpu::BindingResource::Sampler(sampler),
},
],
});
self.pattern_bind_groups
.entry((handle, state))
.or_insert(bg)
}
fn image_bind_group_for(
&mut self,
image: (TextureHandle, SamplerState),
shadow: Option<(TextureHandle, SamplerState)>,
) -> &wgpu::BindGroup {
let key: ImageBindGroupKey = (image.0, image.1, shadow);
if self.image_bind_groups.contains_key(&key) {
return self
.image_bind_groups
.get(&key)
.expect("just checked contains_key");
}
self.samplers
.entry(image.1)
.or_insert_with(|| build_sampler(&self.device, image.1));
if let Some((_, sstate)) = shadow {
self.samplers
.entry(sstate)
.or_insert_with(|| build_sampler(&self.device, sstate));
}
let image_view = self
.textures
.get(&image.0)
.map(|t| &t.view)
.expect("image_bind_group_for: unknown image TextureHandle");
let image_sampler = &self.samplers[&image.1];
let (shadow_view, shadow_sampler) = match shadow {
Some((handle, sstate)) => {
let view = self
.textures
.get(&handle)
.map(|t| &t.view)
.expect("image_bind_group_for: unknown shadow TextureHandle");
(view, &self.samplers[&sstate])
}
None => (&self.dummy_view, &self.dummy_sampler),
};
let bg = self.device.create_bind_group(&wgpu::BindGroupDescriptor {
label: Some("noesis_runtime image+shadow bg"),
layout: &self.image_bind_group_layout,
entries: &[
wgpu::BindGroupEntry {
binding: 0,
resource: wgpu::BindingResource::TextureView(image_view),
},
wgpu::BindGroupEntry {
binding: 1,
resource: wgpu::BindingResource::Sampler(image_sampler),
},
wgpu::BindGroupEntry {
binding: 2,
resource: wgpu::BindingResource::TextureView(shadow_view),
},
wgpu::BindGroupEntry {
binding: 3,
resource: wgpu::BindingResource::Sampler(shadow_sampler),
},
],
});
self.image_bind_groups.entry(key).or_insert(bg)
}
pub fn set_onscreen_target(&mut self, view: wgpu::TextureView, width: u32, height: u32) {
assert_eq!(
self.phase,
FramePhase::Idle,
"set_onscreen_target called while a frame phase is active",
);
let need_alloc = self
.onscreen_stencil
.as_ref()
.is_none_or(|s| s.width != width || s.height != height);
if need_alloc {
let texture = self.device.create_texture(&wgpu::TextureDescriptor {
label: Some("noesis_runtime onscreen stencil"),
size: wgpu::Extent3d {
width,
height,
depth_or_array_layers: 1,
},
mip_level_count: 1,
sample_count: 1,
dimension: wgpu::TextureDimension::D2,
format: RT_STENCIL_FORMAT,
usage: wgpu::TextureUsages::RENDER_ATTACHMENT,
view_formats: &[],
});
let view = texture.create_view(&wgpu::TextureViewDescriptor::default());
self.onscreen_stencil = Some(GpuStencil {
texture,
view,
width,
height,
});
}
self.target_view = Some(view);
}
#[must_use]
pub fn texture(&self, handle: TextureHandle) -> Option<&wgpu::Texture> {
self.textures.get(&handle).map(|t| &t.texture)
}
#[must_use]
pub fn render_target_size(&self, handle: RenderTargetHandle) -> Option<(u32, u32)> {
self.render_targets
.get(&handle)
.map(|rt| (rt.width, rt.height))
}
fn alloc_handle(&mut self) -> NonZeroU64 {
let h = self.next_handle;
self.next_handle += 1;
NonZeroU64::new(h).expect("alloc_handle starts at 1")
}
fn upload_uniforms(&mut self, batch: &Batch) -> (u32, u32, u32) {
let cbuf0 = batch.vertex_uniforms[0].as_bytes();
let cbuf1 = batch.vertex_uniforms[1].as_bytes();
let mut vs_buf = [0u8; VS_UNIFORM_SIZE as usize];
let cbuf0_len = cbuf0.len().min(VS_GLYPH_SIZE_OFFSET);
vs_buf[..cbuf0_len].copy_from_slice(&cbuf0[..cbuf0_len]);
let cbuf1_len = cbuf1
.len()
.min(VS_UNIFORM_SIZE as usize - VS_GLYPH_SIZE_OFFSET);
vs_buf[VS_GLYPH_SIZE_OFFSET..VS_GLYPH_SIZE_OFFSET + cbuf1_len]
.copy_from_slice(&cbuf1[..cbuf1_len]);
let vs_offset = self.vs_ring.write(&self.queue, &vs_buf);
let ps_offset = self
.ps_ring
.write(&self.queue, batch.pixel_uniforms[0].as_bytes());
let ps1_offset = self
.ps1_ring
.write(&self.queue, batch.pixel_uniforms[1].as_bytes());
(vs_offset, ps_offset, ps1_offset)
}
}
const fn round_up_to_4(n: usize) -> usize {
(n + 3) & !3
}
const fn align_up_u64(n: u64, align: u64) -> u64 {
(n + align - 1) & !(align - 1)
}
struct UniformRing {
buffer: wgpu::Buffer,
struct_size: u64,
slot_stride: u64,
slot_capacity: u32,
next_slot: u32,
scratch: Vec<u8>,
}
impl UniformRing {
fn new(
device: &wgpu::Device,
label: &str,
struct_size: u64,
slot_capacity: u32,
alignment: u64,
) -> Self {
assert!(
struct_size.is_multiple_of(4),
"uniform struct_size must be a multiple of 4"
);
let slot_stride = align_up_u64(struct_size, alignment);
let buffer = device.create_buffer(&wgpu::BufferDescriptor {
label: Some(label),
size: slot_stride * u64::from(slot_capacity),
usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST,
mapped_at_creation: false,
});
Self {
buffer,
struct_size,
slot_stride,
slot_capacity,
next_slot: 0,
scratch: vec![0u8; struct_size as usize],
}
}
fn buffer(&self) -> &wgpu::Buffer {
&self.buffer
}
fn reset(&mut self) {
self.next_slot = 0;
}
fn write(&mut self, queue: &wgpu::Queue, bytes: &[u8]) -> u32 {
assert!(
self.next_slot < self.slot_capacity,
"uniform ring (struct_size={}) exhausted at {} slots — raise UNIFORM_RING_SLOTS",
self.struct_size,
self.slot_capacity,
);
let slot = self.next_slot;
self.next_slot += 1;
let offset = u64::from(slot) * self.slot_stride;
let len = bytes.len().min(self.struct_size as usize);
self.scratch[..len].copy_from_slice(&bytes[..len]);
self.scratch[len..].fill(0);
queue.write_buffer(&self.buffer, offset, &self.scratch);
u32::try_from(offset).expect("uniform ring offset overflowed u32")
}
}
const fn wgpu_format_for(format: TextureFormat) -> wgpu::TextureFormat {
match format {
TextureFormat::Rgba8 | TextureFormat::Rgbx8 => wgpu::TextureFormat::Rgba8Unorm,
TextureFormat::R8 => wgpu::TextureFormat::R8Unorm,
_ => wgpu::TextureFormat::Rgba8Unorm,
}
}
const fn bytes_per_pixel(format: TextureFormat) -> u32 {
match format {
TextureFormat::Rgba8 | TextureFormat::Rgbx8 => 4,
TextureFormat::R8 => 1,
_ => 4,
}
}
const fn shader_uses_paint_texture(shader: u8) -> bool {
shader == Shader::PATH_PATTERN.0
|| shader == Shader::PATH_AA_PATTERN.0
|| shader == Shader::PATH_PATTERN_CLAMP.0
|| shader == Shader::PATH_AA_PATTERN_CLAMP.0
|| shader == Shader::PATH_PATTERN_REPEAT.0
|| shader == Shader::PATH_AA_PATTERN_REPEAT.0
|| shader == Shader::PATH_PATTERN_MIRROR_U.0
|| shader == Shader::PATH_AA_PATTERN_MIRROR_U.0
|| shader == Shader::PATH_PATTERN_MIRROR_V.0
|| shader == Shader::PATH_AA_PATTERN_MIRROR_V.0
|| shader == Shader::PATH_PATTERN_MIRROR.0
|| shader == Shader::PATH_AA_PATTERN_MIRROR.0
|| shader == Shader::PATH_LINEAR.0
|| shader == Shader::PATH_AA_LINEAR.0
|| shader == Shader::PATH_RADIAL.0
|| shader == Shader::PATH_AA_RADIAL.0
|| shader == Shader::SDF_SOLID.0
|| shader == Shader::SDF_LCD_SOLID.0
|| shader == Shader::OPACITY_LINEAR.0
|| shader == Shader::OPACITY_RADIAL.0
|| shader == Shader::OPACITY_PATTERN.0
|| shader == Shader::OPACITY_PATTERN_CLAMP.0
|| shader == Shader::OPACITY_PATTERN_REPEAT.0
|| shader == Shader::OPACITY_PATTERN_MIRROR_U.0
|| shader == Shader::OPACITY_PATTERN_MIRROR_V.0
|| shader == Shader::OPACITY_PATTERN_MIRROR.0
|| shader == Shader::DOWNSAMPLE.0
|| shader == Shader::UPSAMPLE.0
}
fn batch_paint_texture(batch: &Batch) -> Option<(TextureHandle, SamplerState)> {
match batch.shader.0 {
s if s == Shader::PATH_PATTERN.0
|| s == Shader::PATH_AA_PATTERN.0
|| s == Shader::PATH_PATTERN_CLAMP.0
|| s == Shader::PATH_AA_PATTERN_CLAMP.0
|| s == Shader::PATH_PATTERN_REPEAT.0
|| s == Shader::PATH_AA_PATTERN_REPEAT.0
|| s == Shader::PATH_PATTERN_MIRROR_U.0
|| s == Shader::PATH_AA_PATTERN_MIRROR_U.0
|| s == Shader::PATH_PATTERN_MIRROR_V.0
|| s == Shader::PATH_AA_PATTERN_MIRROR_V.0
|| s == Shader::PATH_PATTERN_MIRROR.0
|| s == Shader::PATH_AA_PATTERN_MIRROR.0
|| s == Shader::OPACITY_PATTERN.0
|| s == Shader::OPACITY_PATTERN_CLAMP.0
|| s == Shader::OPACITY_PATTERN_REPEAT.0
|| s == Shader::OPACITY_PATTERN_MIRROR_U.0
|| s == Shader::OPACITY_PATTERN_MIRROR_V.0
|| s == Shader::OPACITY_PATTERN_MIRROR.0 =>
{
batch.pattern_handle().map(|h| (h, batch.pattern_sampler))
}
s if s == Shader::PATH_LINEAR.0
|| s == Shader::PATH_AA_LINEAR.0
|| s == Shader::PATH_RADIAL.0
|| s == Shader::PATH_AA_RADIAL.0
|| s == Shader::OPACITY_LINEAR.0
|| s == Shader::OPACITY_RADIAL.0 =>
{
batch.ramps_handle().map(|h| (h, batch.ramps_sampler))
}
s if s == Shader::SDF_SOLID.0 || s == Shader::SDF_LCD_SOLID.0 => {
batch.glyphs_handle().map(|h| (h, batch.glyphs_sampler))
}
s if s == Shader::DOWNSAMPLE.0 || s == Shader::UPSAMPLE.0 => {
batch.pattern_handle().map(|h| (h, batch.pattern_sampler))
}
_ => None,
}
}
const fn shader_uses_image_texture(shader: u8) -> bool {
shader == Shader::OPACITY_SOLID.0
|| shader == Shader::OPACITY_LINEAR.0
|| shader == Shader::OPACITY_RADIAL.0
|| shader == Shader::OPACITY_PATTERN.0
|| shader == Shader::OPACITY_PATTERN_CLAMP.0
|| shader == Shader::OPACITY_PATTERN_REPEAT.0
|| shader == Shader::OPACITY_PATTERN_MIRROR_U.0
|| shader == Shader::OPACITY_PATTERN_MIRROR_V.0
|| shader == Shader::OPACITY_PATTERN_MIRROR.0
|| shader == Shader::UPSAMPLE.0
|| shader == Shader::SHADOW.0
|| shader == Shader::BLUR.0
}
const fn shader_uses_shadow_texture(shader: u8) -> bool {
shader == Shader::SHADOW.0 || shader == Shader::BLUR.0
}
fn wgpu_wrap_mode(wrap_raw: u8) -> wgpu::AddressMode {
match wrap_raw {
0 | 1 => wgpu::AddressMode::ClampToEdge,
2 => wgpu::AddressMode::Repeat,
3..=5 => wgpu::AddressMode::MirrorRepeat,
other => panic!("unknown Noesis WrapMode raw value: {other}"),
}
}
fn build_sampler(device: &wgpu::Device, state: SamplerState) -> wgpu::Sampler {
let wrap = wgpu_wrap_mode(state.wrap_mode_raw());
let filter = match state.minmag_filter_raw() {
0 => wgpu::FilterMode::Nearest,
_ => wgpu::FilterMode::Linear,
};
let mipmap_filter = match state.mip_filter_raw() {
0 | 1 => wgpu::FilterMode::Nearest,
_ => wgpu::FilterMode::Linear,
};
let lod_max = match state.mip_filter_raw() {
0 => 0.25, _ => 32.0,
};
device.create_sampler(&wgpu::SamplerDescriptor {
label: Some("noesis_runtime sampler"),
address_mode_u: wrap,
address_mode_v: wrap,
address_mode_w: wrap,
mag_filter: filter,
min_filter: filter,
mipmap_filter,
lod_min_clamp: 0.0,
lod_max_clamp: lod_max,
compare: None,
anisotropy_clamp: 1,
border_color: None,
})
}
impl RenderDevice for WgpuRenderDevice {
fn as_any_mut(&mut self) -> &mut dyn std::any::Any {
self
}
fn caps(&self) -> DeviceCaps {
DeviceCaps {
center_pixel_offset: 0.0,
linear_rendering: false,
subpixel_rendering: false,
depth_range_zero_to_one: true,
clip_space_y_inverted: false,
}
}
fn create_texture(&mut self, desc: TextureDesc<'_>) -> TextureBinding {
let handle = TextureHandle(self.alloc_handle());
let wgpu_format = wgpu_format_for(desc.format);
let texture = self.device.create_texture(&wgpu::TextureDescriptor {
label: Some(desc.label),
size: wgpu::Extent3d {
width: desc.width,
height: desc.height,
depth_or_array_layers: 1,
},
mip_level_count: desc.num_levels,
sample_count: 1,
dimension: wgpu::TextureDimension::D2,
format: wgpu_format,
usage: wgpu::TextureUsages::TEXTURE_BINDING | wgpu::TextureUsages::COPY_DST,
view_formats: &[],
});
if let Some(levels) = desc.data {
assert_eq!(
levels.len() as u32,
desc.num_levels,
"create_texture: data.len() ({}) must equal num_levels ({})",
levels.len(),
desc.num_levels,
);
let bpp = bytes_per_pixel(desc.format);
for (level, bytes) in levels.iter().enumerate() {
let level_u32 = level as u32;
let w = (desc.width >> level_u32).max(1);
let h = (desc.height >> level_u32).max(1);
self.queue.write_texture(
wgpu::TexelCopyTextureInfo {
texture: &texture,
mip_level: level_u32,
origin: wgpu::Origin3d::ZERO,
aspect: wgpu::TextureAspect::All,
},
bytes,
wgpu::TexelCopyBufferLayout {
offset: 0,
bytes_per_row: Some(w * bpp),
rows_per_image: Some(h),
},
wgpu::Extent3d {
width: w,
height: h,
depth_or_array_layers: 1,
},
);
}
}
let view = texture.create_view(&wgpu::TextureViewDescriptor::default());
let has_alpha = !matches!(desc.format, TextureFormat::Rgbx8);
self.textures.insert(
handle,
GpuTexture {
texture,
view,
noesis_format: desc.format,
width: desc.width,
height: desc.height,
num_levels: desc.num_levels,
},
);
TextureBinding {
handle,
width: desc.width,
height: desc.height,
has_mipmaps: desc.num_levels > 1,
inverted: false,
has_alpha,
}
}
fn update_texture(
&mut self,
handle: TextureHandle,
level: u32,
rect: TextureRect,
data: &[u8],
) {
let tex = self
.textures
.get(&handle)
.expect("update_texture: unknown TextureHandle");
let bpp = bytes_per_pixel(tex.noesis_format);
assert!(level < tex.num_levels, "update_texture level out of range");
self.queue.write_texture(
wgpu::TexelCopyTextureInfo {
texture: &tex.texture,
mip_level: level,
origin: wgpu::Origin3d {
x: rect.x,
y: rect.y,
z: 0,
},
aspect: wgpu::TextureAspect::All,
},
data,
wgpu::TexelCopyBufferLayout {
offset: 0,
bytes_per_row: Some(rect.width * bpp),
rows_per_image: Some(rect.height),
},
wgpu::Extent3d {
width: rect.width,
height: rect.height,
depth_or_array_layers: 1,
},
);
}
fn end_updating_textures(&mut self, _textures: &[TextureHandle]) {
}
fn drop_texture(&mut self, handle: TextureHandle) {
self.textures.remove(&handle);
self.pattern_bind_groups.retain(|(h, _), _| *h != handle);
self.image_bind_groups.retain(|(img, _, shadow), _| {
*img != handle && shadow.is_none_or(|(s, _)| s != handle)
});
}
fn create_render_target(&mut self, desc: RenderTargetDesc<'_>) -> RenderTargetBinding {
assert_eq!(
desc.sample_count, 1,
"Phase 4.B only supports sample_count = 1 (use PPAA for anti-aliasing); \
MSAA support lands later",
);
let rt_handle = RenderTargetHandle(self.alloc_handle());
let resolve_handle = TextureHandle(self.alloc_handle());
let color_texture = self.device.create_texture(&wgpu::TextureDescriptor {
label: Some(desc.label),
size: wgpu::Extent3d {
width: desc.width,
height: desc.height,
depth_or_array_layers: 1,
},
mip_level_count: 1,
sample_count: 1,
dimension: wgpu::TextureDimension::D2,
format: RT_COLOR_FORMAT,
usage: wgpu::TextureUsages::RENDER_ATTACHMENT
| wgpu::TextureUsages::TEXTURE_BINDING
| wgpu::TextureUsages::COPY_SRC,
view_formats: &[],
});
let color_view = color_texture.create_view(&wgpu::TextureViewDescriptor::default());
let resolve_view = color_texture.create_view(&wgpu::TextureViewDescriptor::default());
let stencil = desc.needs_stencil.then(|| {
let tex = self.device.create_texture(&wgpu::TextureDescriptor {
label: Some(&format!("{} stencil", desc.label)),
size: wgpu::Extent3d {
width: desc.width,
height: desc.height,
depth_or_array_layers: 1,
},
mip_level_count: 1,
sample_count: 1,
dimension: wgpu::TextureDimension::D2,
format: RT_STENCIL_FORMAT,
usage: wgpu::TextureUsages::RENDER_ATTACHMENT,
view_formats: &[],
});
let view = tex.create_view(&wgpu::TextureViewDescriptor::default());
(tex, view)
});
self.textures.insert(
resolve_handle,
GpuTexture {
texture: color_texture,
view: resolve_view,
noesis_format: TextureFormat::Rgba8,
width: desc.width,
height: desc.height,
num_levels: 1,
},
);
self.render_targets.insert(
rt_handle,
GpuRenderTarget {
color_view,
resolve_handle,
stencil,
width: desc.width,
height: desc.height,
},
);
RenderTargetBinding {
handle: rt_handle,
resolve_texture: TextureBinding {
handle: resolve_handle,
width: desc.width,
height: desc.height,
has_mipmaps: false,
inverted: false,
has_alpha: true,
},
}
}
fn clone_render_target(&mut self, label: &str, src: RenderTargetHandle) -> RenderTargetBinding {
let (width, height, needs_stencil) = {
let src_rt = self
.render_targets
.get(&src)
.expect("clone_render_target: unknown src RenderTargetHandle");
(src_rt.width, src_rt.height, src_rt.stencil.is_some())
};
self.create_render_target(RenderTargetDesc {
label,
width,
height,
sample_count: 1,
needs_stencil,
})
}
fn drop_render_target(&mut self, handle: RenderTargetHandle) {
self.render_targets.remove(&handle);
}
fn begin_offscreen_render(&mut self) {
assert_eq!(
self.phase,
FramePhase::Idle,
"begin_offscreen_render while a frame phase is already active",
);
self.vs_ring.reset();
self.ps_ring.reset();
self.ps1_ring.reset();
self.encoder = Some(
self.device
.create_command_encoder(&wgpu::CommandEncoderDescriptor {
label: Some("noesis_runtime offscreen frame"),
}),
);
self.phase = FramePhase::Offscreen;
self.current_rt = None;
self.current_tile = None;
}
fn end_offscreen_render(&mut self) {
assert_eq!(
self.phase,
FramePhase::Offscreen,
"end_offscreen_render without a matching begin_offscreen_render",
);
let encoder = self.encoder.take().expect("offscreen encoder missing");
self.queue.submit(Some(encoder.finish()));
self.phase = FramePhase::Idle;
self.current_rt = None;
self.current_tile = None;
}
fn begin_onscreen_render(&mut self) {
assert_eq!(
self.phase,
FramePhase::Idle,
"begin_onscreen_render while a frame phase is already active",
);
self.vs_ring.reset();
self.ps_ring.reset();
self.ps1_ring.reset();
self.encoder = Some(
self.device
.create_command_encoder(&wgpu::CommandEncoderDescriptor {
label: Some("noesis_runtime onscreen frame"),
}),
);
self.phase = FramePhase::Onscreen;
self.onscreen_stencil_cleared = false;
}
fn end_onscreen_render(&mut self) {
assert_eq!(
self.phase,
FramePhase::Onscreen,
"end_onscreen_render without a matching begin_onscreen_render",
);
let encoder = self.encoder.take().expect("onscreen encoder missing");
self.queue.submit(Some(encoder.finish()));
self.phase = FramePhase::Idle;
}
fn set_render_target(&mut self, handle: RenderTargetHandle) {
assert_eq!(
self.phase,
FramePhase::Offscreen,
"set_render_target outside offscreen phase",
);
assert!(
self.render_targets.contains_key(&handle),
"set_render_target: unknown RenderTargetHandle",
);
self.current_rt = Some(handle);
self.current_tile = None;
self.current_rt_stencil_cleared = false;
}
fn begin_tile(&mut self, handle: RenderTargetHandle, tile: Tile) {
assert_eq!(
self.current_rt,
Some(handle),
"begin_tile for a handle that isn't the currently-bound RT",
);
self.current_tile = Some(tile);
}
fn end_tile(&mut self, handle: RenderTargetHandle) {
assert_eq!(
self.current_rt,
Some(handle),
"end_tile for a handle that isn't the currently-bound RT",
);
self.current_tile = None;
}
fn resolve_render_target(&mut self, _handle: RenderTargetHandle, _tiles: &[Tile]) {
}
fn map_vertices(&mut self, bytes: u32) -> &mut [u8] {
assert!(
self.vertex_mapped_bytes.is_none(),
"map_vertices without unmap_vertices"
);
self.vertex_mapped_bytes = Some(bytes);
&mut self.vertex_staging[..bytes as usize]
}
fn unmap_vertices(&mut self) {
let bytes = self
.vertex_mapped_bytes
.take()
.expect("unmap_vertices without map_vertices");
let padded = round_up_to_4(bytes as usize);
self.queue
.write_buffer(&self.vertex_buffer, 0, &self.vertex_staging[..padded]);
}
fn map_indices(&mut self, bytes: u32) -> &mut [u8] {
assert!(
self.index_mapped_bytes.is_none(),
"map_indices without unmap_indices"
);
self.index_mapped_bytes = Some(bytes);
&mut self.index_staging[..bytes as usize]
}
fn unmap_indices(&mut self) {
let bytes = self
.index_mapped_bytes
.take()
.expect("unmap_indices without map_indices");
let padded = round_up_to_4(bytes as usize);
self.queue
.write_buffer(&self.index_buffer, 0, &self.index_staging[..padded]);
}
fn draw_batch(&mut self, batch: &Batch) {
let (has_stencil, clear_stencil) = match self.phase {
FramePhase::Onscreen => {
let has = self.onscreen_stencil.is_some();
let clear = has && !self.onscreen_stencil_cleared;
self.onscreen_stencil_cleared = true;
(has, clear)
}
FramePhase::Offscreen => {
let rt_handle = self
.current_rt
.expect("draw_batch during offscreen phase without set_render_target");
let has = self
.render_targets
.get(&rt_handle)
.expect("current_rt dangles")
.stencil
.is_some();
let clear = has && !self.current_rt_stencil_cleared;
self.current_rt_stencil_cleared = true;
(has, clear)
}
FramePhase::Idle => panic!("draw_batch outside begin/end_*_render"),
};
let key = PipelineKey::from_batch(batch, has_stencil);
let (vs_offset, ps_offset, ps1_offset) = self.upload_uniforms(batch);
self.pipelines.ensure(key);
let pattern_slot = if shader_uses_paint_texture(batch.shader.0) {
let slot = self.forced_pattern.unwrap_or_else(|| {
batch_paint_texture(batch).expect(
"paint-texture batch with null texture handle — Noesis should always populate the right slot",
)
});
let _ = self.pattern_bind_group_for(slot.0, slot.1);
Some(slot)
} else {
None
};
let image_slot: Option<ImageBindGroupKey> = if shader_uses_image_texture(batch.shader.0) {
let image = self.forced_image.unwrap_or_else(|| {
let handle = batch.image_handle().expect(
"OPACITY/UPSAMPLE/SHADOW/BLUR batch with null image handle — Noesis should populate batch.image",
);
(handle, batch.image_sampler)
});
let shadow = if shader_uses_shadow_texture(batch.shader.0) {
Some(self.forced_shadow.unwrap_or_else(|| {
let handle = batch.shadow_handle().expect(
"SHADOW/BLUR batch with null shadow handle — Noesis should populate batch.shadow",
);
(handle, batch.shadow_sampler)
}))
} else {
None
};
let _ = self.image_bind_group_for(image, shadow);
Some((image.0, image.1, shadow))
} else {
None
};
let stride = u64::from(SIZE_FOR_FORMAT[key.vertex_format as usize]);
let vertex_offset = u64::from(batch.vertex_offset);
let vertex_byte_count = u64::from(batch.num_vertices) * stride;
let index_byte_offset = u64::from(batch.start_index) * 2;
let index_byte_count = u64::from(batch.num_indices) * 2;
let (target_view, scissor, stencil_view) = match self.phase {
FramePhase::Onscreen => {
let view = self
.target_view
.as_ref()
.expect("onscreen draw without set_onscreen_target");
let stencil = self.onscreen_stencil.as_ref().map(|s| &s.view);
(view, None, stencil)
}
FramePhase::Offscreen => {
let rt_handle = self
.current_rt
.expect("draw_batch during offscreen phase without set_render_target");
let rt = self
.render_targets
.get(&rt_handle)
.expect("current_rt dangles");
let scissor = self.current_tile.map(|t| {
let y_top = rt.height.saturating_sub(t.y + t.height);
(t.x, y_top, t.width, t.height)
});
let stencil = rt.stencil.as_ref().map(|(_, view)| view);
(&rt.color_view, scissor, stencil)
}
FramePhase::Idle => panic!("draw_batch outside begin/end_*_render"),
};
debug_assert_eq!(
has_stencil,
stencil_view.is_some(),
"has_stencil must agree with the resolved stencil view",
);
let pipeline = self.pipelines.get(key);
let vertex_buffer = &self.vertex_buffer;
let index_buffer = &self.index_buffer;
let vs_bg = &self.vs_uniform_bind_group;
let ps_bg = &self.ps_uniform_bind_group;
let pattern_bg = if let Some(slot) = pattern_slot {
self.pattern_bind_groups
.get(&slot)
.expect("pattern bind group not cached")
} else {
&self.dummy_pattern_bg
};
let image_bg = if let Some(slot) = image_slot {
self.image_bind_groups
.get(&slot)
.expect("image bind group not cached")
} else {
&self.dummy_image_bg
};
let encoder = self
.encoder
.as_mut()
.expect("draw_batch outside begin/end_*_render");
let depth_stencil_attachment =
stencil_view.map(|view| wgpu::RenderPassDepthStencilAttachment {
view,
depth_ops: None,
stencil_ops: Some(wgpu::Operations {
load: if clear_stencil {
wgpu::LoadOp::Clear(0)
} else {
wgpu::LoadOp::Load
},
store: wgpu::StoreOp::Store,
}),
});
let mut rpass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
label: Some("noesis_runtime draw_batch"),
color_attachments: &[Some(wgpu::RenderPassColorAttachment {
view: target_view,
depth_slice: None,
resolve_target: None,
ops: wgpu::Operations {
load: wgpu::LoadOp::Load,
store: wgpu::StoreOp::Store,
},
})],
depth_stencil_attachment,
timestamp_writes: None,
occlusion_query_set: None,
});
rpass.set_pipeline(pipeline);
if has_stencil {
rpass.set_stencil_reference(u32::from(batch.stencil_ref));
}
rpass.set_bind_group(0, vs_bg, &[vs_offset]);
rpass.set_bind_group(1, ps_bg, &[ps_offset, ps1_offset]);
rpass.set_bind_group(2, pattern_bg, &[]);
rpass.set_bind_group(3, image_bg, &[]);
if let Some((x, y, w, h)) = scissor {
rpass.set_scissor_rect(x, y, w, h);
}
rpass.set_vertex_buffer(
0,
vertex_buffer.slice(vertex_offset..vertex_offset + vertex_byte_count),
);
rpass.set_index_buffer(
index_buffer.slice(index_byte_offset..index_byte_offset + index_byte_count),
wgpu::IndexFormat::Uint16,
);
rpass.draw_indexed(0..batch.num_indices, 0, 0..1);
}
}