use crate::vertex::{InstanceData, Vertex};
use cvkg_core::Rect;
use lru::LruCache;
use std::num::NonZeroUsize;
use std::sync::Arc;
pub mod budget;
pub mod golden;
pub mod lod;
pub mod shader_features;
pub mod thermal;
pub mod virtualization;
pub use budget::OffscreenBudget;
pub use golden::{GoldenImageComparator, GoldenImageConfig, GoldenImageResult};
pub use lod::EffectLod;
pub use shader_features::ShaderFeatureFlags;
pub use thermal::{ThermalConfig, ThermalState};
pub use virtualization::{Frustum, SpatialCell, SpatialHash};
#[derive(Clone, Debug)]
pub struct SvgModel {
pub vertices: Vec<Vertex>,
pub indices: Vec<u32>,
pub view_box: Rect,
pub paths: Vec<SvgPath>,
pub animations: Vec<SvgAnimation>,
}
#[derive(Clone, Debug)]
pub struct SvgPath {
pub id: String,
pub vertex_range: std::ops::Range<usize>,
pub index_range: std::ops::Range<usize>,
pub local_transform: SvgTransform,
}
#[derive(Clone, Debug, Default)]
pub struct SvgTransform {
pub translate: [f32; 2],
pub rotation: f32,
pub scale: f32,
}
#[derive(Clone, Debug)]
pub struct SvgAnimation {
pub target_id: String,
pub attribute_name: String,
pub keyframe_values: Vec<f32>,
pub key_times: Vec<f32>,
pub duration: f32,
pub vertex_range: std::ops::Range<usize>,
}
impl SvgAnimation {
pub fn evaluate(&self, t: f32) -> f32 {
let vals = &self.keyframe_values;
if vals.is_empty() {
return 0.0;
}
if vals.len() == 1 {
return vals[0];
}
if vals.len() == 2 {
return vals[0] + (vals[1] - vals[0]) * t;
}
let times = if self.key_times.len() == vals.len() {
&self.key_times
} else {
return self.evaluate_uniform(t);
};
let t = t.clamp(0.0, 1.0);
for i in 0..times.len() - 1 {
if t >= times[i] && t <= times[i + 1] {
let seg_t = (t - times[i]) / (times[i + 1] - times[i]);
return vals[i] + (vals[i + 1] - vals[i]) * seg_t;
}
}
vals[vals.len() - 1]
}
fn evaluate_uniform(&self, t: f32) -> f32 {
let vals = &self.keyframe_values;
let n = vals.len() - 1;
let t = t.clamp(0.0, 1.0);
let idx_f = t * n as f32;
let idx = idx_f.floor() as usize;
let frac = idx_f - idx as f32;
if idx >= n {
vals[n]
} else {
vals[idx] + (vals[idx + 1] - vals[idx]) * frac
}
}
}
#[derive(Debug, Clone)]
pub(crate) struct DrawCall {
pub texture_id: Option<u32>,
pub scissor_rect: Option<Rect>,
pub index_start: u32,
pub index_count: u32,
pub instance_count: u32,
pub material: cvkg_core::DrawMaterial,
pub target_id: Option<u64>,
pub panel_id: Option<u64>,
pub instance_start: u32,
pub draw_order: i32,
}
#[derive(Debug, Clone)]
pub(crate) struct MemoEntry {
pub hash: u64,
pub frame_gen: u64,
pub vertices: Vec<crate::vertex::Vertex>,
pub indices: Vec<u32>,
pub instance_data: Vec<crate::vertex::InstanceData>,
pub draw_calls: Vec<DrawCall>,
}
pub struct OffscreenEffectConfig {
pub target_id: u64,
pub effect: String,
pub blend_mode: u32,
pub effect_args: [f32; 16],
}
#[derive(Debug, Clone, Copy)]
pub(crate) struct ShadowState {
pub radius: f32,
pub color: [f32; 4],
pub _offset: [f32; 2],
}
pub(crate) struct SurfaceContext {
pub(crate) surface: wgpu::Surface<'static>,
pub(crate) config: wgpu::SurfaceConfiguration,
pub(crate) scene_texture: wgpu::TextureView,
pub(crate) scene_msaa_texture: wgpu::TextureView,
pub(crate) scene_bind_group: wgpu::BindGroup,
pub(crate) scene_texture_bind_group: wgpu::BindGroup,
pub(crate) depth_texture_view: wgpu::TextureView,
pub(crate) blur_tex_a: crate::kvasir::resource::ResourceId,
pub(crate) blur_tex_b: crate::kvasir::resource::ResourceId,
pub(crate) bloom_tex_a: crate::kvasir::resource::ResourceId,
pub(crate) bloom_tex_b: crate::kvasir::resource::ResourceId,
pub(crate) blur_env_bind_group_a: wgpu::BindGroup,
pub(crate) blur_env_bind_group_b: wgpu::BindGroup,
pub(crate) bloom_env_bind_group_a: wgpu::BindGroup,
pub(crate) bloom_env_bind_group_b: wgpu::BindGroup,
pub(crate) scale_factor: f32,
pub(crate) sampler: wgpu::Sampler,
}
pub struct HeadlessContext {
pub scene_texture: wgpu::TextureView,
pub scene_msaa_texture: wgpu::TextureView,
pub scene_bind_group: wgpu::BindGroup,
pub scene_texture_bind_group: wgpu::BindGroup,
pub depth_texture_view: wgpu::TextureView,
pub blur_tex_a: crate::kvasir::resource::ResourceId,
pub blur_tex_b: crate::kvasir::resource::ResourceId,
pub bloom_tex_a: crate::kvasir::resource::ResourceId,
pub bloom_tex_b: crate::kvasir::resource::ResourceId,
pub blur_env_bind_group_a: wgpu::BindGroup,
pub blur_env_bind_group_b: wgpu::BindGroup,
pub bloom_env_bind_group_a: wgpu::BindGroup,
pub bloom_env_bind_group_b: wgpu::BindGroup,
pub scale_factor: f32,
pub sampler: wgpu::Sampler,
pub width: u32,
pub height: u32,
pub output_texture: wgpu::Texture,
pub output_view: wgpu::TextureView,
}
pub(crate) const MAX_VERTICES: usize = 100_000;
pub(crate) const MAX_INDICES: usize = 150_000;
pub(crate) const MAX_PARTICLES: usize = 65536;
#[repr(C)]
#[derive(Copy, Clone, Debug, Default, bytemuck::Pod, bytemuck::Zeroable)]
pub struct GpuParticle {
pub pos_vel: [f32; 4],
pub color_life: [f32; 4],
}
#[repr(C)]
#[derive(Copy, Clone, Debug, Default, bytemuck::Pod, bytemuck::Zeroable)]
pub struct ParticleUniforms {
pub dt: f32,
pub _pad: [f32; 7],
}
#[repr(C)]
#[derive(Copy, Clone, Debug, bytemuck::Pod, bytemuck::Zeroable)]
pub struct EffectUniforms {
pub time: f32,
pub pad0: f32,
pub size: [f32; 2],
pub args: [f32; 16],
}
#[repr(C)]
#[derive(Copy, Clone, bytemuck::Pod, bytemuck::Zeroable)]
pub struct GlassInstanceUniforms {
pub tint_override: [f32; 4],
pub ior_override: f32,
pub blur_multiplier: f32,
pub frost_override: f32,
pub scissor_px: [f32; 4],
pub portal_index: f32,
pub _pad: f32,
}
impl Default for GlassInstanceUniforms {
fn default() -> Self {
Self {
tint_override: [0.0; 4],
ior_override: 0.0,
blur_multiplier: 1.0,
frost_override: 0.0,
scissor_px: [0.0; 4],
portal_index: 0.0,
_pad: 0.0,
}
}
}
pub struct GeometryBuffers {
pub vertex_buffer: wgpu::Buffer,
pub index_buffer: wgpu::Buffer,
pub instance_buffer: wgpu::Buffer,
pub max_vertices: usize,
pub max_indices: usize,
}
impl GeometryBuffers {
pub fn forge(device: &wgpu::Device, max_vertices: usize, max_indices: usize) -> Self {
let vertex_buffer = device.create_buffer(&wgpu::BufferDescriptor {
label: Some("Surtr Vertex Anvil"),
size: (max_vertices * std::mem::size_of::<Vertex>()) as u64,
usage: wgpu::BufferUsages::VERTEX | wgpu::BufferUsages::COPY_DST,
mapped_at_creation: false,
});
let index_buffer = device.create_buffer(&wgpu::BufferDescriptor {
label: Some("Surtr Index Anvil"),
size: (max_indices * std::mem::size_of::<u32>()) as u64,
usage: wgpu::BufferUsages::INDEX | wgpu::BufferUsages::COPY_DST,
mapped_at_creation: false,
});
let instance_buffer = device.create_buffer(&wgpu::BufferDescriptor {
label: Some("Surtr Instance Anvil"),
size: (max_vertices / 4 * std::mem::size_of::<InstanceData>()) as u64,
usage: wgpu::BufferUsages::VERTEX | wgpu::BufferUsages::COPY_DST,
mapped_at_creation: false,
});
Self {
vertex_buffer,
index_buffer,
instance_buffer,
max_vertices,
max_indices,
}
}
pub fn vram_bytes(&self) -> u64 {
let vertex_bytes = self.max_vertices * std::mem::size_of::<Vertex>();
let index_bytes = self.max_indices * std::mem::size_of::<u32>();
let instance_bytes = (self.max_vertices / 4) * std::mem::size_of::<InstanceData>();
(vertex_bytes + index_bytes + instance_bytes) as u64
}
pub fn grow_vertex_buffer(
&mut self,
device: &wgpu::Device,
min_capacity: usize,
max_capacity: usize,
) -> bool {
let current = self.vertex_buffer.size() as usize / std::mem::size_of::<Vertex>();
if min_capacity <= current {
return false;
}
let new_capacity = min_capacity.min(max_capacity);
if new_capacity <= current {
return false;
}
self.vertex_buffer = device.create_buffer(&wgpu::BufferDescriptor {
label: Some("Vertex Buffer (Grown)"),
size: (new_capacity * std::mem::size_of::<Vertex>()) as u64,
usage: wgpu::BufferUsages::VERTEX | wgpu::BufferUsages::COPY_DST,
mapped_at_creation: false,
});
true
}
pub fn grow_index_buffer(
&mut self,
device: &wgpu::Device,
min_capacity: usize,
max_capacity: usize,
) -> bool {
let current = self.index_buffer.size() as usize / std::mem::size_of::<u32>();
if min_capacity <= current {
return false;
}
let new_capacity = min_capacity.min(max_capacity);
if new_capacity <= current {
return false;
}
self.index_buffer = device.create_buffer(&wgpu::BufferDescriptor {
label: Some("Index Buffer (Grown)"),
size: (new_capacity * std::mem::size_of::<u32>()) as u64,
usage: wgpu::BufferUsages::INDEX | wgpu::BufferUsages::COPY_DST,
mapped_at_creation: false,
});
true
}
}
pub struct TextSubsystem {
pub engine: cvkg_runic_text::TextEngine,
pub glyph_cache: LruCache<u64, (cvkg_core::Rect, f32, f32, f32, f32)>,
pub shaped_cache: LruCache<(String, u32), std::sync::Arc<cvkg_runic_text::ShapedText>>,
pub atlas_size: u32,
}
impl TextSubsystem {
pub fn forge(glyph_cache_capacity: NonZeroUsize) -> Self {
Self::with_atlas_size(glyph_cache_capacity, 4096)
}
pub fn with_atlas_size(glyph_cache_capacity: NonZeroUsize, atlas_size: u32) -> Self {
Self {
engine: cvkg_runic_text::TextEngine::default(),
glyph_cache: LruCache::new(glyph_cache_capacity),
shaped_cache: LruCache::new(NonZeroUsize::new(2048).unwrap()),
atlas_size,
}
}
pub fn atlas_size(&self) -> u32 {
self.atlas_size
}
pub fn clear_caches(&mut self) {
self.shaped_cache.clear();
}
}
pub struct SvgSubsystem {
pub model_cache: LruCache<String, SvgModel>,
pub tree_cache: LruCache<String, usvg::Tree>,
pub filter_engine: Option<cvkg_svg_filters::FilterEngine>,
pub filter_batches: Vec<cvkg_svg_filters::FilterNode>,
dirty_elements: std::collections::HashSet<String>,
dirty_sources: std::collections::HashSet<String>,
}
impl SvgSubsystem {
pub fn forge(
device: &Arc<wgpu::Device>,
queue: &Arc<wgpu::Queue>,
model_cache_capacity: NonZeroUsize,
tree_cache_capacity: NonZeroUsize,
) -> Self {
let filter_engine = cvkg_svg_filters::FilterEngine::new(cvkg_svg_filters::GpuContext {
device: device.clone(),
queue: queue.clone(),
})
.ok();
Self {
model_cache: LruCache::new(model_cache_capacity),
tree_cache: LruCache::new(tree_cache_capacity),
filter_engine,
filter_batches: Vec::new(),
dirty_elements: std::collections::HashSet::new(),
dirty_sources: std::collections::HashSet::new(),
}
}
pub fn clear_filter_batches(&mut self) {
self.filter_batches.clear();
}
pub fn mark_element_dirty(&mut self, element_id: &str) {
self.dirty_elements.insert(element_id.to_string());
}
pub fn mark_source_dirty(&mut self, source_name: &str) {
self.dirty_sources.insert(source_name.to_string());
self.model_cache.pop(source_name);
}
pub fn is_element_dirty(&self, element_id: &str) -> bool {
self.dirty_elements.contains(element_id) || self.dirty_sources.contains(element_id)
}
pub fn is_source_dirty(&self, source_name: &str) -> bool {
self.dirty_sources.contains(source_name)
}
pub fn clear_dirty(&mut self) {
self.dirty_elements.clear();
self.dirty_sources.clear();
}
pub fn dirty_count(&self) -> usize {
self.dirty_elements.len() + self.dirty_sources.len()
}
}
pub struct ParticleSubsystem {
pub staging: Vec<GpuParticle>,
pub count: u32,
pub write_head: u32,
pub last_compact: std::time::Instant,
}
impl ParticleSubsystem {
pub fn forge() -> Self {
Self {
staging: Vec::new(),
count: 0,
write_head: 0,
last_compact: std::time::Instant::now(),
}
}
}
#[cfg(test)]
mod p1_1_geometry_buffers_tests {
use super::*;
#[test]
fn vram_bytes_is_sum_of_three_buffers() {
let max_vertices = 1000usize;
let max_indices = 1500usize;
let vertex_bytes = max_vertices * std::mem::size_of::<Vertex>();
let index_bytes = max_indices * std::mem::size_of::<u32>();
let instance_bytes = (max_vertices / 4) * std::mem::size_of::<InstanceData>();
let expected = (vertex_bytes + index_bytes + instance_bytes) as u64;
assert!(expected > 0, "expected vram bytes > 0");
assert!(std::mem::size_of::<Vertex>() >= 16);
assert!(std::mem::size_of::<InstanceData>() >= 16);
}
#[test]
fn size_of_vertex_is_known() {
let size = std::mem::size_of::<Vertex>();
assert_eq!(size % 4, 0, "Vertex size must be 4-byte aligned");
}
}
#[cfg(test)]
mod p1_1_text_subsystem_tests {
use super::TextSubsystem;
use std::num::NonZeroUsize;
#[test]
fn forge_creates_glyph_cache_with_given_capacity() {
let cap = NonZeroUsize::new(100).unwrap();
let subsystem = TextSubsystem::forge(cap);
assert_eq!(subsystem.glyph_cache.cap().get(), 100);
assert!(subsystem.shaped_cache.is_empty());
}
#[test]
fn clear_caches_empties_shaped_but_keeps_glyph() {
let cap = NonZeroUsize::new(10).unwrap();
let mut subsystem = TextSubsystem::forge(cap);
subsystem.clear_caches();
assert!(subsystem.shaped_cache.is_empty());
assert_eq!(subsystem.glyph_cache.cap().get(), 10);
}
#[test]
fn text_subsystem_default_atlas_size() {
use std::num::NonZeroUsize;
let sub = TextSubsystem::forge(NonZeroUsize::new(1024).unwrap());
assert_eq!(sub.atlas_size(), 4096, "Default atlas size should be 4096");
}
#[test]
fn text_subsystem_custom_atlas_size() {
use std::num::NonZeroUsize;
let sub = TextSubsystem::with_atlas_size(NonZeroUsize::new(1024).unwrap(), 2048);
assert_eq!(sub.atlas_size(), 2048, "Custom atlas size should be 2048");
}
#[test]
fn default_capacity_is_8192_matching_p1_5() {
let cap = NonZeroUsize::new(8192).unwrap();
let subsystem = TextSubsystem::forge(cap);
assert_eq!(subsystem.glyph_cache.cap().get(), 8192);
}
}
#[cfg(test)]
mod p1_1_particle_subsystem_tests {
use super::ParticleSubsystem;
#[test]
fn forge_creates_empty_state() {
let p = ParticleSubsystem::forge();
assert!(p.staging.is_empty());
assert_eq!(p.count, 0);
assert_eq!(p.write_head, 0);
}
#[test]
fn fields_are_publicly_mutable() {
let mut p = ParticleSubsystem::forge();
p.staging.push(Default::default());
p.count = 1;
p.write_head = 1;
assert_eq!(p.staging.len(), 1);
assert_eq!(p.count, 1);
assert_eq!(p.write_head, 1);
}
}
#[cfg(test)]
mod p1_24_incremental_svg_tests {
use super::SvgSubsystem;
use std::num::NonZeroUsize;
use std::sync::Arc;
#[test]
fn dirty_count_starts_at_zero() {
let dirty_elements: std::collections::HashSet<String> = std::collections::HashSet::new();
let dirty_sources: std::collections::HashSet<String> = std::collections::HashSet::new();
assert_eq!(dirty_elements.len() + dirty_sources.len(), 0);
}
#[test]
fn mark_dirty_increments_count() {
let mut dirty = std::collections::HashSet::new();
dirty.insert("path1".to_string());
dirty.insert("path2".to_string());
assert_eq!(dirty.len(), 2);
}
#[test]
fn source_dirty_implies_all_elements_dirty() {
let mut sources: std::collections::HashSet<String> = std::collections::HashSet::new();
sources.insert("my_icon.svg".to_string());
assert!(sources.contains("my_icon.svg"));
assert!(!sources.contains("other.svg"));
}
}