use std::collections::{HashMap, HashSet};
use std::num::NonZeroU64;
use log::warn;
#[cfg(feature = "shim")]
use noesis_runtime::render_device::RenderDevice;
use noesis_runtime::render_device::types::{
Batch, DeviceCaps, SIZE_FOR_FORMAT, SamplerState, Shader, TextureFormat, Tile,
};
use noesis_runtime::render_device::{
RenderTargetBinding, RenderTargetDesc, RenderTargetHandle, TextureBinding, TextureDesc,
TextureHandle, TextureRect,
};
use crate::custom_shader::{
BatchShader, MAX_SHADER_CONSTANTS, MAX_SHADER_TEXTURES, PixelShaderDesc, PixelShaderError,
PixelShaderHandle,
};
use crate::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 = MAX_SHADER_CONSTANTS as u64;
const PS_UNIFORM1_BUILTIN_SIZE: usize = 32;
const PATTERN_LOD_SIZE: u64 = 16;
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_stream: GeometryStream,
index_stream: GeometryStream,
uniforms: UniformRings,
paint_layout: wgpu::BindGroupLayout,
custom_paint_layout: wgpu::BindGroupLayout,
image_layout: wgpu::BindGroupLayout,
samplers: HashMap<SamplerState, wgpu::Sampler>,
texture_groups: HashMap<TextureGroupKey, wgpu::BindGroup>,
#[allow(dead_code)] dummy_texture: wgpu::Texture,
dummy_view: wgpu::TextureView,
dummy_sampler: wgpu::Sampler,
pipelines: PipelineCache,
warned_shaders: HashSet<u8>,
warned_custom_shaders: HashSet<PixelShaderHandle>,
stats: DeviceStats,
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,
pass: Option<OpenPass>,
encoder: Option<wgpu::CommandEncoder>,
current_rt: Option<RenderTargetHandle>,
current_tile: Option<Tile>,
next_handle: u64,
}
#[derive(Copy, Clone, Debug, PartialEq, Eq)]
enum PassTarget {
Onscreen,
Offscreen(RenderTargetHandle),
}
struct OpenPass {
pass: wgpu::RenderPass<'static>,
target: PassTarget,
scissor: Option<(u32, u32, u32, u32)>,
}
#[derive(Copy, Clone, Debug, PartialEq, Eq)]
enum SkippedDraw {
Protocol,
UnsupportedShader,
}
type TextureSlot = Option<(TextureHandle, SamplerState)>;
const MAX_GROUP_SLOTS: usize = 1 + MAX_SHADER_TEXTURES;
type TextureGroupKey = (TextureGroup, [TextureSlot; MAX_GROUP_SLOTS]);
#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash)]
enum TextureGroup {
Paint,
CustomPaint,
Image,
}
impl TextureGroup {
const fn slots(self) -> usize {
match self {
Self::Paint => 1,
Self::CustomPaint => MAX_GROUP_SLOTS,
Self::Image => 3,
}
}
}
struct GpuTexture {
texture: wgpu::Texture,
view: wgpu::TextureView,
noesis_format: Option<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,
resolve_view: wgpu::TextureView,
#[allow(dead_code)] msaa: Option<wgpu::Texture>,
stencil: Option<(wgpu::Texture, wgpu::TextureView)>,
sample_count: u32,
width: u32,
height: u32,
}
const MSAA_SAMPLE_COUNT: u32 = 4;
#[derive(Copy, Clone, Debug, PartialEq)]
pub struct PatternLod {
pub bias: f32,
pub max_level: f32,
}
impl Default for PatternLod {
fn default() -> Self {
Self {
bias: 0.0,
max_level: f32::MAX,
}
}
}
impl PatternLod {
fn to_bytes(self) -> [u8; PATTERN_LOD_SIZE as usize] {
let max_level = self.max_level.min(64.0);
let mut bytes = [0u8; PATTERN_LOD_SIZE as usize];
bytes[..4].copy_from_slice(&self.bias.to_le_bytes());
bytes[4..8].copy_from_slice(&max_level.to_le_bytes());
bytes
}
}
impl WgpuRenderDevice {
#[must_use]
#[allow(clippy::too_many_lines)] pub fn new(device: wgpu::Device, queue: wgpu::Queue) -> Self {
let vertex_stream = GeometryStream::new(
&device,
"noesis_wgpu vertex stream",
DYNAMIC_VB_SIZE,
wgpu::BufferUsages::VERTEX | wgpu::BufferUsages::COPY_DST,
);
let index_stream = GeometryStream::new(
&device,
"noesis_wgpu index stream",
DYNAMIC_IB_SIZE,
wgpu::BufferUsages::INDEX | wgpu::BufferUsages::COPY_DST,
);
let uniforms = UniformRings::new(&device, &queue);
let paint_layout = texture_group_layout(&device, TextureGroup::Paint);
let custom_paint_layout = texture_group_layout(&device, TextureGroup::CustomPaint);
let image_layout = texture_group_layout(&device, TextureGroup::Image);
let dummy_texture = device.create_texture(&wgpu::TextureDescriptor {
label: Some("noesis_wgpu dummy texture"),
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_wgpu dummy sampler"),
..Default::default()
});
let pipeline_layout = |label, paint_layout| {
device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
label: Some(label),
bind_group_layouts: &[
Some(&uniforms.vs_layout),
Some(&uniforms.ps_layout),
Some(paint_layout),
Some(&image_layout),
],
immediate_size: 0,
})
};
let pipelines = PipelineCache::new(
device.clone(),
pipeline_layout("noesis_wgpu pipeline layout", &paint_layout),
pipeline_layout("noesis_wgpu custom pipeline layout", &custom_paint_layout),
RT_COLOR_FORMAT,
);
Self {
device,
queue,
vertex_stream,
index_stream,
uniforms,
paint_layout,
custom_paint_layout,
image_layout,
samplers: HashMap::new(),
texture_groups: HashMap::new(),
dummy_texture,
dummy_view,
dummy_sampler,
pipelines,
warned_shaders: HashSet::new(),
warned_custom_shaders: HashSet::new(),
stats: DeviceStats::default(),
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,
pass: None,
encoder: None,
current_rt: None,
current_tile: None,
next_handle: 1,
}
}
fn ensure_texture_group(&mut self, key: TextureGroupKey) -> bool {
if self.texture_groups.contains_key(&key) {
return true;
}
let (group, slots) = key;
let slots = &slots[..group.slots()];
for (_, state) in slots.iter().flatten() {
self.samplers
.entry(*state)
.or_insert_with(|| build_sampler(&self.device, *state));
}
let mut resources = Vec::with_capacity(slots.len());
for slot in slots {
resources.push(match slot {
Some((handle, state)) => {
let Some(texture) = self.textures.get(handle) else {
return false;
};
(&texture.view, &self.samplers[state])
}
None => (&self.dummy_view, &self.dummy_sampler),
});
}
let entries: Vec<_> = (0u32..)
.zip(&resources)
.flat_map(|(i, (view, sampler))| {
[
wgpu::BindGroupEntry {
binding: 2 * i,
resource: wgpu::BindingResource::TextureView(view),
},
wgpu::BindGroupEntry {
binding: 2 * i + 1,
resource: wgpu::BindingResource::Sampler(sampler),
},
]
})
.collect();
let layout = match group {
TextureGroup::Paint => &self.paint_layout,
TextureGroup::CustomPaint => &self.custom_paint_layout,
TextureGroup::Image => &self.image_layout,
};
let bind_group = self.device.create_bind_group(&wgpu::BindGroupDescriptor {
label: Some("noesis_wgpu textures"),
layout,
entries: &entries,
});
self.texture_groups.insert(key, bind_group);
true
}
pub fn set_onscreen_target(&mut self, view: wgpu::TextureView, width: u32, height: u32) {
if width == 0 || height == 0 {
warn!("set_onscreen_target: {width}x{height} target ignored");
return;
}
if self.phase != FramePhase::Idle {
warn!(
"set_onscreen_target called inside the {:?} phase",
self.phase
);
}
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_wgpu 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.onscreen_stencil_cleared = false;
}
self.pass = None;
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 stats(&self) -> DeviceStats {
DeviceStats {
pipelines: self.pipelines.len(),
..self.stats
}
}
#[must_use]
pub fn render_target_size(&self, handle: RenderTargetHandle) -> Option<(u32, u32)> {
self.render_targets
.get(&handle)
.map(|rt| (rt.width, rt.height))
}
pub fn set_pattern_lod(&mut self, lod: PatternLod) {
self.queue
.write_buffer(&self.uniforms.pattern_lod, 0, &lod.to_bytes());
}
pub fn create_pixel_shader(
&mut self,
desc: PixelShaderDesc<'_>,
) -> Result<PixelShaderHandle, PixelShaderError> {
let handle = PixelShaderHandle(self.alloc_handle());
self.pipelines.register_custom(handle, &desc)?;
Ok(handle)
}
pub fn drop_pixel_shader(&mut self, handle: PixelShaderHandle) {
self.pipelines.remove_custom(handle);
self.warned_custom_shaders.remove(&handle);
}
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, ps1: (&[u8], usize)) -> (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]);
self.uniforms.write(
&self.device,
&self.queue,
&vs_buf,
batch.pixel_uniforms[0].as_bytes(),
ps1,
)
}
}
fn tile_scissor(tile: Tile, width: u32, height: u32) -> (u32, u32, u32, u32) {
let x = tile.x.min(width);
let w = tile.width.min(width - x);
let bottom = tile.y.min(height);
let h = tile.height.min(height - bottom);
(x, height - bottom - h, w, h)
}
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,
label: &'static str,
struct_size: u64,
slot_stride: u64,
slot_capacity: u32,
next_slot: u32,
flushed_slot: u32,
staged: Vec<u8>,
}
impl UniformRing {
fn new(
device: &wgpu::Device,
label: &'static str,
struct_size: u64,
slot_capacity: u32,
) -> Self {
let alignment = u64::from(device.limits().min_uniform_buffer_offset_alignment);
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,
label,
struct_size,
slot_stride,
slot_capacity,
next_slot: 0,
flushed_slot: 0,
staged: vec![0u8; (slot_stride * u64::from(slot_capacity)) as usize],
}
}
fn is_full(&self) -> bool {
self.next_slot == self.slot_capacity
}
fn grow(&mut self, device: &wgpu::Device) {
*self = Self::new(device, self.label, self.struct_size, self.slot_capacity * 2);
}
fn reset(&mut self) {
self.next_slot = 0;
self.flushed_slot = 0;
}
fn flush(&mut self, queue: &wgpu::Queue) {
if self.next_slot == self.flushed_slot {
return;
}
let start = u64::from(self.flushed_slot) * self.slot_stride;
let end = u64::from(self.next_slot) * self.slot_stride;
queue.write_buffer(
&self.buffer,
start,
&self.staged[start as usize..end as usize],
);
self.flushed_slot = self.next_slot;
}
fn write(&mut self, bytes: &[u8], len: usize) -> u32 {
let slot = self.next_slot;
self.next_slot += 1;
let offset = u64::from(slot) * self.slot_stride;
let start = offset as usize;
let copied = bytes.len().min(len);
self.staged[start..start + copied].copy_from_slice(&bytes[..copied]);
self.staged[start + copied..start + len].fill(0);
u32::try_from(offset).expect("uniform ring offset overflowed u32")
}
}
struct UniformRings {
vs: UniformRing,
ps0: UniformRing,
ps1: UniformRing,
pattern_lod: wgpu::Buffer,
vs_layout: wgpu::BindGroupLayout,
ps_layout: wgpu::BindGroupLayout,
vs_bind_group: wgpu::BindGroup,
ps_bind_group: wgpu::BindGroup,
}
impl UniformRings {
fn new(device: &wgpu::Device, queue: &wgpu::Queue) -> Self {
let vs = UniformRing::new(
device,
"noesis_wgpu vs_uniforms ring (mat4 projection)",
VS_UNIFORM_SIZE,
UNIFORM_RING_SLOTS,
);
let ps0 = UniformRing::new(
device,
"noesis_wgpu ps_uniforms0 ring (cbuffer0_ps[8])",
PS_UNIFORM0_SIZE,
UNIFORM_RING_SLOTS,
);
let ps1 = UniformRing::new(
device,
"noesis_wgpu ps_uniforms1 ring (cbuffer1_ps)",
PS_UNIFORM1_SIZE,
UNIFORM_RING_SLOTS,
);
let pattern_lod = device.create_buffer(&wgpu::BufferDescriptor {
label: Some("noesis_wgpu pattern lod"),
size: PATTERN_LOD_SIZE,
usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST,
mapped_at_creation: false,
});
queue.write_buffer(&pattern_lod, 0, &PatternLod::default().to_bytes());
let uniform_entry = |binding, visibility, size| wgpu::BindGroupLayoutEntry {
binding,
visibility,
ty: wgpu::BindingType::Buffer {
ty: wgpu::BufferBindingType::Uniform,
has_dynamic_offset: true,
min_binding_size: NonZeroU64::new(size),
},
count: None,
};
let vs_layout = device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
label: Some("noesis_wgpu vs_uniforms layout"),
entries: &[uniform_entry(
0,
wgpu::ShaderStages::VERTEX,
VS_UNIFORM_SIZE,
)],
});
let ps_layout = device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
label: Some("noesis_wgpu ps_uniforms layout"),
entries: &[
uniform_entry(0, wgpu::ShaderStages::FRAGMENT, PS_UNIFORM0_SIZE),
uniform_entry(1, wgpu::ShaderStages::FRAGMENT, PS_UNIFORM1_SIZE),
wgpu::BindGroupLayoutEntry {
binding: 2,
visibility: wgpu::ShaderStages::FRAGMENT,
ty: wgpu::BindingType::Buffer {
ty: wgpu::BufferBindingType::Uniform,
has_dynamic_offset: false,
min_binding_size: NonZeroU64::new(PATTERN_LOD_SIZE),
},
count: None,
},
],
});
let vs_bind_group = vs_bind_group(device, &vs_layout, &vs);
let ps_bind_group = ps_bind_group(device, &ps_layout, &ps0, &ps1, &pattern_lod);
Self {
vs,
ps0,
ps1,
pattern_lod,
vs_layout,
ps_layout,
vs_bind_group,
ps_bind_group,
}
}
fn reset(&mut self) {
self.vs.reset();
self.ps0.reset();
self.ps1.reset();
}
fn flush(&mut self, queue: &wgpu::Queue) {
self.vs.flush(queue);
self.ps0.flush(queue);
self.ps1.flush(queue);
}
fn write(
&mut self,
device: &wgpu::Device,
queue: &wgpu::Queue,
vs: &[u8],
ps0: &[u8],
ps1: (&[u8], usize),
) -> (u32, u32, u32) {
if self.vs.is_full() {
self.flush(queue);
self.vs.grow(device);
self.ps0.grow(device);
self.ps1.grow(device);
self.vs_bind_group = vs_bind_group(device, &self.vs_layout, &self.vs);
self.ps_bind_group = ps_bind_group(
device,
&self.ps_layout,
&self.ps0,
&self.ps1,
&self.pattern_lod,
);
}
(
self.vs.write(vs, VS_UNIFORM_SIZE as usize),
self.ps0.write(ps0, PS_UNIFORM0_SIZE as usize),
self.ps1.write(ps1.0, ps1.1),
)
}
}
fn ring_binding(ring: &UniformRing) -> wgpu::BindingResource<'_> {
wgpu::BindingResource::Buffer(wgpu::BufferBinding {
buffer: &ring.buffer,
offset: 0,
size: NonZeroU64::new(ring.struct_size),
})
}
fn vs_bind_group(
device: &wgpu::Device,
layout: &wgpu::BindGroupLayout,
vs: &UniformRing,
) -> wgpu::BindGroup {
device.create_bind_group(&wgpu::BindGroupDescriptor {
label: Some("noesis_wgpu vs_uniforms"),
layout,
entries: &[wgpu::BindGroupEntry {
binding: 0,
resource: ring_binding(vs),
}],
})
}
fn ps_bind_group(
device: &wgpu::Device,
layout: &wgpu::BindGroupLayout,
ps0: &UniformRing,
ps1: &UniformRing,
pattern_lod: &wgpu::Buffer,
) -> wgpu::BindGroup {
device.create_bind_group(&wgpu::BindGroupDescriptor {
label: Some("noesis_wgpu ps_uniforms"),
layout,
entries: &[
wgpu::BindGroupEntry {
binding: 0,
resource: ring_binding(ps0),
},
wgpu::BindGroupEntry {
binding: 1,
resource: ring_binding(ps1),
},
wgpu::BindGroupEntry {
binding: 2,
resource: pattern_lod.as_entire_binding(),
},
],
})
}
fn texture_group_layout(device: &wgpu::Device, group: TextureGroup) -> wgpu::BindGroupLayout {
let entries: Vec<_> = (0u32..)
.take(group.slots())
.flat_map(|i| {
[
wgpu::BindGroupLayoutEntry {
binding: 2 * i,
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: 2 * i + 1,
visibility: wgpu::ShaderStages::FRAGMENT,
ty: wgpu::BindingType::Sampler(wgpu::SamplerBindingType::Filtering),
count: None,
},
]
})
.collect();
device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
label: Some(&format!("noesis_wgpu {group:?} textures")),
entries: &entries,
})
}
struct GeometryStream {
buffer: wgpu::Buffer,
label: &'static str,
usage: wgpu::BufferUsages,
staging: Vec<u8>,
mapped_bytes: Option<u32>,
cursor: u64,
segment_base: u64,
}
impl GeometryStream {
fn new(
device: &wgpu::Device,
label: &'static str,
size: u64,
usage: wgpu::BufferUsages,
) -> Self {
let buffer = device.create_buffer(&wgpu::BufferDescriptor {
label: Some(label),
size,
usage,
mapped_at_creation: false,
});
Self {
buffer,
label,
usage,
staging: vec![0u8; size as usize],
mapped_bytes: None,
cursor: 0,
segment_base: 0,
}
}
fn buffer(&self) -> &wgpu::Buffer {
&self.buffer
}
fn segment_base(&self) -> u64 {
self.segment_base
}
fn written_end(&self) -> u64 {
self.cursor
}
fn reset(&mut self) {
self.cursor = 0;
}
fn map(&mut self, bytes: u32) -> &mut [u8] {
if self.mapped_bytes.is_some() {
warn!(
"{}: map without unmap; the earlier map is discarded",
self.label
);
}
let len = bytes as usize;
let padded = round_up_to_4(len);
if padded > self.staging.len() {
self.staging.resize(padded, 0);
}
self.mapped_bytes = Some(bytes);
&mut self.staging[..len]
}
fn unmap(&mut self, device: &wgpu::Device, queue: &wgpu::Queue) {
let Some(bytes) = self.mapped_bytes.take() else {
warn!("{}: unmap without map", self.label);
return;
};
let padded = round_up_to_4(bytes as usize) as u64;
if self.cursor + padded > self.buffer.size() {
let new_size = (self.cursor + padded).max(self.buffer.size() * 2);
self.buffer = device.create_buffer(&wgpu::BufferDescriptor {
label: Some(self.label),
size: new_size,
usage: self.usage,
mapped_at_creation: false,
});
}
self.segment_base = self.cursor;
queue.write_buffer(&self.buffer, self.cursor, &self.staging[..padded as usize]);
self.cursor += padded;
}
}
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_LINEAR.0
|| shader == Shader::SDF_RADIAL.0
|| shader == Shader::SDF_PATTERN.0
|| shader == Shader::SDF_PATTERN_CLAMP.0
|| shader == Shader::SDF_PATTERN_REPEAT.0
|| shader == Shader::SDF_PATTERN_MIRROR_U.0
|| shader == Shader::SDF_PATTERN_MIRROR_V.0
|| shader == Shader::SDF_PATTERN_MIRROR.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
|| shader == Shader::CUSTOM_EFFECT.0
}
fn batch_paint_texture(
batch: &Batch,
textures: BatchTextures,
) -> 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::SDF_PATTERN.0
|| s == Shader::SDF_PATTERN_CLAMP.0
|| s == Shader::SDF_PATTERN_REPEAT.0
|| s == Shader::SDF_PATTERN_MIRROR_U.0
|| s == Shader::SDF_PATTERN_MIRROR_V.0
|| s == Shader::SDF_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 =>
{
textures.pattern.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::SDF_LINEAR.0
|| s == Shader::SDF_RADIAL.0
|| s == Shader::OPACITY_LINEAR.0
|| s == Shader::OPACITY_RADIAL.0 =>
{
textures.ramps.map(|h| (h, batch.ramps_sampler))
}
s if s == Shader::DOWNSAMPLE.0
|| s == Shader::UPSAMPLE.0
|| s == Shader::CUSTOM_EFFECT.0 =>
{
textures.pattern.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
}
const fn shader_uses_glyphs_texture(shader: u8) -> bool {
shader >= Shader::SDF_SOLID.0 && shader <= Shader::SDF_LCD_SOLID.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 => {
warn_once!("unknown Noesis WrapMode raw value {other}; using ClampToEdge");
wgpu::AddressMode::ClampToEdge
}
}
}
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::MipmapFilterMode::Nearest,
_ => wgpu::MipmapFilterMode::Linear,
};
let lod_max = match state.mip_filter_raw() {
0 => 0.25, _ => 32.0,
};
device.create_sampler(&wgpu::SamplerDescriptor {
label: Some("noesis_wgpu 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 WgpuRenderDevice {
pub fn caps(&self) -> DeviceCaps {
DeviceCaps {
linear_rendering: false,
subpixel_rendering: false,
depth_range_zero_to_one: true,
clip_space_y_inverted: false,
}
}
pub 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 {
if levels.len() != desc.num_levels as usize {
warn!(
"create_texture '{}': {} data levels for {} mip levels",
desc.label,
levels.len(),
desc.num_levels,
);
}
let bpp = bytes_per_pixel(desc.format);
for (level, bytes) in levels.iter().take(desc.num_levels as usize).enumerate() {
let level_u32 = level as u32;
let w = (desc.width >> level_u32).max(1);
let h = (desc.height >> level_u32).max(1);
let needed = u64::from(w) * u64::from(h) * u64::from(bpp);
if (bytes.len() as u64) < needed {
warn!(
"create_texture '{}': level {level} has {} bytes, needs {needed}; \
leaving it unwritten",
desc.label,
bytes.len(),
);
continue;
}
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: Some(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,
}
}
pub fn update_texture(
&mut self,
handle: TextureHandle,
level: u32,
rect: TextureRect,
data: &[u8],
) {
let Some(tex) = self.textures.get(&handle) else {
warn!("update_texture: unknown texture {handle:?}");
return;
};
let Some(format) = tex.noesis_format else {
warn!("update_texture: {handle:?} is imported; the host writes it");
return;
};
if level >= tex.num_levels {
warn!(
"update_texture: level {level} of a texture with {} levels",
tex.num_levels
);
return;
}
let level_w = (tex.width >> level).max(1);
let level_h = (tex.height >> level).max(1);
if rect.x.saturating_add(rect.width) > level_w
|| rect.y.saturating_add(rect.height) > level_h
{
warn!("update_texture: rect {rect:?} outside level {level} ({level_w}x{level_h})");
return;
}
let bpp = bytes_per_pixel(format);
let needed = u64::from(rect.width) * u64::from(rect.height) * u64::from(bpp);
if (data.len() as u64) < needed {
warn!(
"update_texture: {} bytes for a {}x{} rect that needs {needed}",
data.len(),
rect.width,
rect.height,
);
return;
}
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,
},
);
}
pub fn end_updating_textures(&mut self, _textures: &[TextureHandle]) {}
pub fn drop_texture(&mut self, handle: TextureHandle) {
self.textures.remove(&handle);
self.texture_groups.retain(|(_, slots), _| {
!slots
.iter()
.flatten()
.any(|(texture, _)| *texture == handle)
});
}
pub fn import_texture(&mut self, texture: &wgpu::Texture) -> Option<TextureBinding> {
let filterable = matches!(
texture
.format()
.sample_type(None, Some(self.device.features())),
Some(wgpu::TextureSampleType::Float { filterable: true })
);
if texture.dimension() != wgpu::TextureDimension::D2
|| texture.depth_or_array_layers() != 1
|| texture.sample_count() != 1
|| !texture
.usage()
.contains(wgpu::TextureUsages::TEXTURE_BINDING)
|| !filterable
{
warn!(
"import_texture: a {:?} {:?} texture with {} layers, {} samples and {:?} \
can't be sampled by Noesis",
texture.format(),
texture.dimension(),
texture.depth_or_array_layers(),
texture.sample_count(),
texture.usage(),
);
return None;
}
let handle = TextureHandle(self.alloc_handle());
let (width, height, num_levels) =
(texture.width(), texture.height(), texture.mip_level_count());
self.textures.insert(
handle,
GpuTexture {
texture: texture.clone(),
view: texture.create_view(&wgpu::TextureViewDescriptor::default()),
noesis_format: None,
width,
height,
num_levels,
},
);
Some(TextureBinding {
handle,
width,
height,
has_mipmaps: num_levels > 1,
inverted: false,
has_alpha: true,
})
}
pub fn create_render_target(&mut self, desc: RenderTargetDesc<'_>) -> RenderTargetBinding {
let sample_count = if desc.sample_count > 1 {
MSAA_SAMPLE_COUNT
} else {
1
};
if desc.sample_count > 1 && desc.sample_count != MSAA_SAMPLE_COUNT {
warn_once!(
"create_render_target: {} samples requested; MSAA targets use {MSAA_SAMPLE_COUNT}",
desc.sample_count,
);
}
let rt_handle = RenderTargetHandle(self.alloc_handle());
let resolve_handle = TextureHandle(self.alloc_handle());
let size = wgpu::Extent3d {
width: desc.width,
height: desc.height,
depth_or_array_layers: 1,
};
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 resolve_view = color_texture.create_view(&wgpu::TextureViewDescriptor::default());
let msaa = (sample_count > 1).then(|| {
self.device.create_texture(&wgpu::TextureDescriptor {
label: Some(&format!("{} msaa", desc.label)),
size,
mip_level_count: 1,
sample_count,
dimension: wgpu::TextureDimension::D2,
format: RT_COLOR_FORMAT,
usage: wgpu::TextureUsages::RENDER_ATTACHMENT,
view_formats: &[],
})
});
let color_view = msaa.as_ref().map_or_else(
|| resolve_view.clone(),
|msaa| msaa.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,
mip_level_count: 1,
sample_count,
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.clone(),
noesis_format: Some(TextureFormat::Rgba8),
width: desc.width,
height: desc.height,
num_levels: 1,
},
);
self.render_targets.insert(
rt_handle,
GpuRenderTarget {
color_view,
resolve_view,
msaa,
stencil,
sample_count,
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,
},
}
}
pub fn clone_render_target(
&mut self,
label: &str,
src: RenderTargetHandle,
) -> RenderTargetBinding {
let (width, height, sample_count, needs_stencil) = match self.render_targets.get(&src) {
Some(rt) => (rt.width, rt.height, rt.sample_count, rt.stencil.is_some()),
None => {
warn!(
"clone_render_target '{label}': unknown source {src:?}; creating a 1x1 target"
);
(1, 1, 1, false)
}
};
self.create_render_target(RenderTargetDesc {
label,
width,
height,
sample_count,
needs_stencil,
})
}
pub fn drop_render_target(&mut self, handle: RenderTargetHandle) {
self.render_targets.remove(&handle);
}
pub fn begin_offscreen_render(&mut self) {
if self.phase != FramePhase::Idle {
warn!(
"begin_offscreen_render found phase {:?} (previous frame aborted \
mid-callback); resetting",
self.phase,
);
}
self.pass = None;
self.uniforms.reset();
self.vertex_stream.reset();
self.index_stream.reset();
self.encoder = Some(
self.device
.create_command_encoder(&wgpu::CommandEncoderDescriptor {
label: Some("noesis_wgpu offscreen frame"),
}),
);
self.phase = FramePhase::Offscreen;
self.current_rt = None;
self.current_tile = None;
}
pub fn end_offscreen_render(&mut self) {
if self.phase != FramePhase::Offscreen {
warn!(
"end_offscreen_render found phase {:?}, expected Offscreen",
self.phase,
);
}
self.pass = None;
self.uniforms.flush(&self.queue);
if let Some(encoder) = self.encoder.take() {
self.queue.submit(Some(encoder.finish()));
}
self.phase = FramePhase::Idle;
self.current_rt = None;
self.current_tile = None;
}
pub fn begin_onscreen_render(&mut self) {
if self.phase != FramePhase::Idle {
warn!(
"begin_onscreen_render found phase {:?} (previous frame aborted \
mid-callback); resetting",
self.phase,
);
}
self.pass = None;
self.uniforms.reset();
self.vertex_stream.reset();
self.index_stream.reset();
self.encoder = Some(
self.device
.create_command_encoder(&wgpu::CommandEncoderDescriptor {
label: Some("noesis_wgpu onscreen frame"),
}),
);
self.phase = FramePhase::Onscreen;
self.onscreen_stencil_cleared = false;
}
pub fn end_onscreen_render(&mut self) {
if self.phase != FramePhase::Onscreen {
warn!(
"end_onscreen_render found phase {:?}, expected Onscreen",
self.phase,
);
}
self.pass = None;
self.uniforms.flush(&self.queue);
if let Some(encoder) = self.encoder.take() {
self.queue.submit(Some(encoder.finish()));
}
self.phase = FramePhase::Idle;
}
pub fn set_render_target(&mut self, handle: RenderTargetHandle) {
self.pass = None;
if self.phase != FramePhase::Offscreen {
warn!("set_render_target called in the {:?} phase", self.phase);
}
if self.render_targets.contains_key(&handle) {
self.current_rt = Some(handle);
} else {
warn!(
"set_render_target: unknown render target {handle:?}; draws skip until the next one"
);
self.current_rt = None;
}
self.current_tile = None;
self.current_rt_stencil_cleared = false;
}
pub fn begin_tile(&mut self, handle: RenderTargetHandle, tile: Tile) {
if self.current_rt != Some(handle) {
warn!("begin_tile for {handle:?}, which isn't the current render target");
}
self.current_tile = Some(tile);
}
pub fn end_tile(&mut self, handle: RenderTargetHandle) {
if self.current_rt != Some(handle) {
warn!("end_tile for {handle:?}, which isn't the current render target");
}
self.current_tile = None;
}
pub fn resolve_render_target(&mut self, handle: RenderTargetHandle, _tiles: &[Tile]) {
let Some(rt) = self.render_targets.get(&handle) else {
warn!("resolve_render_target: unknown render target {handle:?}");
return;
};
if rt.sample_count == 1 {
return;
}
self.pass = None;
let Some(encoder) = self.encoder.as_mut() else {
warn!("resolve_render_target outside begin/end_offscreen_render; skipping");
return;
};
encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
label: Some("noesis_wgpu resolve"),
color_attachments: &[Some(wgpu::RenderPassColorAttachment {
view: &rt.color_view,
depth_slice: None,
resolve_target: Some(&rt.resolve_view),
ops: wgpu::Operations {
load: wgpu::LoadOp::Load,
store: wgpu::StoreOp::Store,
},
})],
depth_stencil_attachment: None,
timestamp_writes: None,
occlusion_query_set: None,
multiview_mask: None,
});
}
pub fn map_vertices(&mut self, bytes: u32) -> &mut [u8] {
self.vertex_stream.map(bytes)
}
pub fn unmap_vertices(&mut self) {
self.vertex_stream.unmap(&self.device, &self.queue);
}
pub fn map_indices(&mut self, bytes: u32) -> &mut [u8] {
self.index_stream.map(bytes)
}
pub fn unmap_indices(&mut self) {
self.index_stream.unmap(&self.device, &self.queue);
}
pub fn draw_batch_with(&mut self, batch: &Batch, textures: BatchTextures) {
if batch.pixel_shader.is_null() {
self.count_draw(batch, textures, None);
} else {
warn_once!("batch has a custom pixel shader; draw it with draw_custom_batch");
self.stats.unsupported_shader_draws += 1;
}
}
pub fn draw_custom_batch(
&mut self,
batch: &Batch,
textures: BatchTextures,
shader: BatchShader<'_>,
) {
self.count_draw(batch, textures, Some(shader));
}
fn count_draw(&mut self, batch: &Batch, textures: BatchTextures, shader: Option<BatchShader>) {
match self.record_draw(batch, textures, shader) {
Ok(()) => self.stats.draws += 1,
Err(SkippedDraw::Protocol) => self.stats.dropped_draws += 1,
Err(SkippedDraw::UnsupportedShader) => self.stats.unsupported_shader_draws += 1,
}
}
#[allow(clippy::too_many_lines)] fn record_draw(
&mut self,
batch: &Batch,
textures: BatchTextures,
custom: Option<BatchShader>,
) -> Result<(), SkippedDraw> {
let (has_stencil, clear_stencil, sample_count) = match self.phase {
FramePhase::Onscreen => {
if self.target_view.is_none() {
warn_once!("onscreen draw without set_onscreen_target; skipping");
return Err(SkippedDraw::Protocol);
}
let has = self.onscreen_stencil.is_some();
(has, has && !self.onscreen_stencil_cleared, 1)
}
FramePhase::Offscreen => {
let Some(rt) = self.current_rt.and_then(|h| self.render_targets.get(&h)) else {
warn_once!("offscreen draw without a live render target; skipping");
return Err(SkippedDraw::Protocol);
};
let has = rt.stencil.is_some();
(
has,
has && !self.current_rt_stencil_cleared,
rt.sample_count,
)
}
FramePhase::Idle => {
warn_once!("draw outside begin/end_*_render; skipping");
return Err(SkippedDraw::Protocol);
}
};
let declared = match custom {
Some(custom) => {
let Some(shader) = self.pipelines.custom(custom.shader) else {
self.warn_custom_shader(custom.shader, "isn't registered");
return Err(SkippedDraw::UnsupportedShader);
};
Some((shader.textures as usize, shader.constants as usize))
}
None if batch.shader == Shader::CUSTOM_EFFECT => {
self.warn_unsupported_shader(batch.shader.0);
return Err(SkippedDraw::UnsupportedShader);
}
None => None,
};
let pixel_shader = custom.map(|custom| custom.shader);
let Some(key) = PipelineKey::from_batch(batch, has_stencil, sample_count, pixel_shader)
else {
self.warn_unsupported_shader(batch.shader.0);
return Err(SkippedDraw::UnsupportedShader);
};
let shader = batch.shader.0;
let paint = if shader_uses_paint_texture(shader) {
let Some(slot) = batch_paint_texture(batch, textures) else {
warn_once!("batch's shader samples a paint texture it wasn't given; skipping");
return Err(SkippedDraw::Protocol);
};
Some(slot)
} else {
None
};
let mut paint_slots = [None; MAX_GROUP_SLOTS];
paint_slots[0] = paint;
let paint_key = match (custom, declared) {
(Some(custom), Some((declared_textures, _))) => {
if custom.textures[..declared_textures].contains(&None) {
warn_once!("custom shader draw lacks an extra texture it declared; skipping");
return Err(SkippedDraw::Protocol);
}
paint_slots[1..].copy_from_slice(&custom.textures);
(TextureGroup::CustomPaint, paint_slots)
}
_ => (TextureGroup::Paint, paint_slots),
};
let image_slot = |uses: bool, texture: Option<TextureHandle>, sampler| {
if !uses {
return Ok(None);
}
texture
.map(|t| Some((t, sampler)))
.ok_or(SkippedDraw::Protocol)
};
let (Ok(image), Ok(shadow), Ok(glyphs)) = (
image_slot(
shader_uses_image_texture(shader),
textures.image,
batch.image_sampler,
),
image_slot(
shader_uses_shadow_texture(shader),
textures.shadow,
batch.shadow_sampler,
),
image_slot(
shader_uses_glyphs_texture(shader),
textures.glyphs,
batch.glyphs_sampler,
),
) else {
warn_once!(
"batch's shader samples an image, shadow or glyph texture it wasn't given; \
skipping"
);
return Err(SkippedDraw::Protocol);
};
let image_key = (TextureGroup::Image, [image, shadow, glyphs, None, None]);
if !self.ensure_texture_group(paint_key) || !self.ensure_texture_group(image_key) {
warn_once!("batch names an unknown texture; skipping");
return Err(SkippedDraw::Protocol);
}
if !self.pipelines.ensure(key) {
match pixel_shader {
Some(handle) => self
.warn_custom_shader(handle, &format!("can't draw as Noesis shader {shader}")),
None => self.warn_unsupported_shader(shader),
}
return Err(SkippedDraw::UnsupportedShader);
}
let stride = u64::from(SIZE_FOR_FORMAT[key.vertex_format as usize]);
let vertex_offset = self.vertex_stream.segment_base() + u64::from(batch.vertex_offset);
let vertex_byte_count = u64::from(batch.num_vertices) * stride;
let index_byte_offset = self.index_stream.segment_base() + u64::from(batch.start_index) * 2;
let index_byte_count = u64::from(batch.num_indices) * 2;
if vertex_offset + vertex_byte_count > self.vertex_stream.written_end()
|| index_byte_offset + index_byte_count > self.index_stream.written_end()
{
warn_once!("batch geometry runs past the mapped buffers; skipping");
return Err(SkippedDraw::Protocol);
}
let ps1 = match (custom, declared) {
(Some(custom), Some((_, constants))) => (
custom
.constants
.unwrap_or_else(|| batch.pixel_uniforms[1].as_bytes()),
constants.next_multiple_of(16),
),
_ => (batch.pixel_uniforms[1].as_bytes(), PS_UNIFORM1_BUILTIN_SIZE),
};
let (vs_offset, ps_offset, ps1_offset) = self.upload_uniforms(batch, ps1);
let (target, target_view, scissor, full, stencil_view) =
if self.phase == FramePhase::Offscreen {
let Some(handle) = self.current_rt else {
return Err(SkippedDraw::Protocol);
};
let Some(rt) = self.render_targets.get(&handle) else {
return Err(SkippedDraw::Protocol);
};
self.current_rt_stencil_cleared = true;
let scissor = self
.current_tile
.map(|t| tile_scissor(t, rt.width, rt.height));
let stencil = rt.stencil.as_ref().map(|(_, view)| view);
(
PassTarget::Offscreen(handle),
&rt.color_view,
scissor,
(0, 0, rt.width, rt.height),
stencil,
)
} else {
self.onscreen_stencil_cleared = true;
let Some(view) = self.target_view.as_ref() else {
return Err(SkippedDraw::Protocol);
};
let stencil = self.onscreen_stencil.as_ref().map(|s| &s.view);
(PassTarget::Onscreen, view, None, (0, 0, 0, 0), stencil)
};
let pipeline = self.pipelines.get(key);
let vertex_buffer = self.vertex_stream.buffer();
let index_buffer = self.index_stream.buffer();
let vs_bg = &self.uniforms.vs_bind_group;
let ps_bg = &self.uniforms.ps_bind_group;
let paint_bg = &self.texture_groups[&paint_key];
let image_bg = &self.texture_groups[&image_key];
let reuse = !clear_stencil && self.pass.as_ref().is_some_and(|open| open.target == target);
if !reuse {
self.pass = None;
let Some(encoder) = self.encoder.as_mut() else {
warn_once!("draw with no open command encoder; skipping");
return Err(SkippedDraw::Protocol);
};
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 pass = encoder
.begin_render_pass(&wgpu::RenderPassDescriptor {
label: Some("noesis_wgpu 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,
multiview_mask: None,
})
.forget_lifetime();
self.pass = Some(OpenPass {
pass,
target,
scissor: None,
});
}
let Some(open) = self.pass.as_mut() else {
return Err(SkippedDraw::Protocol);
};
if open.scissor != scissor {
let (x, y, w, h) = scissor.unwrap_or(full);
open.pass.set_scissor_rect(x, y, w, h);
open.scissor = scissor;
}
let rpass = &mut open.pass;
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, paint_bg, &[]);
rpass.set_bind_group(3, image_bg, &[]);
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);
Ok(())
}
fn warn_unsupported_shader(&mut self, shader: u8) {
if self.warned_shaders.insert(shader) {
warn!("Noesis shader {shader} isn't implemented; skipping its batches");
}
}
fn warn_custom_shader(&mut self, handle: PixelShaderHandle, why: &str) {
if self.warned_custom_shaders.insert(handle) {
warn!("custom shader {handle:?} {why}; skipping its batches");
}
}
}
#[derive(Copy, Clone, Debug, Default, PartialEq, Eq, Hash)]
pub struct DeviceStats {
pub draws: u64,
pub dropped_draws: u64,
pub unsupported_shader_draws: u64,
pub pipelines: usize,
}
impl std::ops::Sub for DeviceStats {
type Output = Self;
fn sub(self, rhs: Self) -> Self {
Self {
draws: self.draws.saturating_sub(rhs.draws),
dropped_draws: self.dropped_draws.saturating_sub(rhs.dropped_draws),
unsupported_shader_draws: self
.unsupported_shader_draws
.saturating_sub(rhs.unsupported_shader_draws),
pipelines: self.pipelines,
}
}
}
#[derive(Copy, Clone, Debug, Default, PartialEq, Eq, Hash)]
pub struct BatchTextures {
pub pattern: Option<TextureHandle>,
pub ramps: Option<TextureHandle>,
pub image: Option<TextureHandle>,
pub glyphs: Option<TextureHandle>,
pub shadow: Option<TextureHandle>,
}
impl BatchTextures {
#[cfg(feature = "shim")]
#[must_use]
pub fn from_batch(batch: &Batch) -> Self {
Self {
pattern: batch.pattern_handle(),
ramps: batch.ramps_handle(),
image: batch.image_handle(),
glyphs: batch.glyphs_handle(),
shadow: batch.shadow_handle(),
}
}
}
#[cfg(feature = "shim")]
impl RenderDevice for WgpuRenderDevice {
fn as_any_mut(&mut self) -> &mut dyn std::any::Any {
self
}
fn caps(&self) -> DeviceCaps {
Self::caps(self)
}
fn create_texture(&mut self, desc: TextureDesc<'_>) -> TextureBinding {
Self::create_texture(self, desc)
}
fn update_texture(
&mut self,
handle: TextureHandle,
level: u32,
rect: TextureRect,
data: &[u8],
) {
Self::update_texture(self, handle, level, rect, data);
}
fn end_updating_textures(&mut self, textures: &[TextureHandle]) {
Self::end_updating_textures(self, textures);
}
fn drop_texture(&mut self, handle: TextureHandle) {
Self::drop_texture(self, handle);
}
fn create_render_target(&mut self, desc: RenderTargetDesc<'_>) -> RenderTargetBinding {
Self::create_render_target(self, desc)
}
fn clone_render_target(&mut self, label: &str, src: RenderTargetHandle) -> RenderTargetBinding {
Self::clone_render_target(self, label, src)
}
fn drop_render_target(&mut self, handle: RenderTargetHandle) {
Self::drop_render_target(self, handle);
}
fn begin_offscreen_render(&mut self) {
Self::begin_offscreen_render(self);
}
fn end_offscreen_render(&mut self) {
Self::end_offscreen_render(self);
}
fn begin_onscreen_render(&mut self) {
Self::begin_onscreen_render(self);
}
fn end_onscreen_render(&mut self) {
Self::end_onscreen_render(self);
}
fn set_render_target(&mut self, handle: RenderTargetHandle) {
Self::set_render_target(self, handle);
}
fn begin_tile(&mut self, handle: RenderTargetHandle, tile: Tile) {
Self::begin_tile(self, handle, tile);
}
fn end_tile(&mut self, handle: RenderTargetHandle) {
Self::end_tile(self, handle);
}
fn resolve_render_target(&mut self, handle: RenderTargetHandle, tiles: &[Tile]) {
Self::resolve_render_target(self, handle, tiles);
}
fn map_vertices(&mut self, bytes: u32) -> &mut [u8] {
Self::map_vertices(self, bytes)
}
fn unmap_vertices(&mut self) {
Self::unmap_vertices(self);
}
fn map_indices(&mut self, bytes: u32) -> &mut [u8] {
Self::map_indices(self, bytes)
}
fn unmap_indices(&mut self) {
Self::unmap_indices(self);
}
fn draw_batch(&mut self, batch: &Batch) {
let textures = BatchTextures::from_batch(batch);
match BatchShader::from_batch(batch) {
Some(shader) => self.draw_custom_batch(batch, textures, shader),
None => self.draw_batch_with(batch, textures),
}
}
}