use super::GpuRenderer;
use super::context_helpers::create_surface_context;
use crate::types::{DrawCall, MAX_PARTICLES};
use crate::vertex::{InstanceData, InstanceData3D, Vertex, Vertex3D};
use cvkg_core::{Rect, Renderer};
use std::sync::Arc;
impl GpuRenderer {
pub fn begin_frame_headless(&mut self) -> wgpu::CommandEncoder {
self.current_window = None;
self.compositor_index_cursor = self.indices.len() as u32;
self.reset_frame_state();
self.staging_belt.recall();
let ctx = self
.headless_context
.as_ref()
.expect("Headless context not initialized");
let time = self.start_time.elapsed().as_secs_f32();
let logical_w = ctx.width as f32 / ctx.scale_factor;
let logical_h = ctx.height as f32 / ctx.scale_factor;
let dt = time - self.current_scene.time;
self.current_scene.time = time;
self.current_scene.delta_time = dt;
self.current_scene.resolution = [logical_w, logical_h];
self.current_scene.scale_factor = ctx.scale_factor;
self.current_scene.proj =
glam::Mat4::orthographic_lh(0.0, logical_w, logical_h, 0.0, -1000.0, 1000.0);
self.queue.write_buffer(
&self.scene_buffer,
0,
bytemuck::bytes_of(&self.current_scene),
);
self.device
.create_command_encoder(&wgpu::CommandEncoderDescriptor {
label: Some("Surtr Headless Command Encoder"),
})
}
fn reset_frame_state(&mut self) {
self.vertices.clear();
self.indices.clear();
self.instance_data.clear();
self.draw_calls.clear();
self.svg.clear_filter_batches();
self.shared_elements.clear();
self.current_texture_id = None;
self.current_panel_id = None;
self.panel_stack.clear();
self.world_space_panels.clear();
self.opacity_stack.clear();
self.opacity_stack.push(1.0);
self.clip_stack.clear();
self.slice_stack.clear();
self.transform_stack.clear();
self.portal_regions.clear();
self.hologram_instances.clear();
self.pending_directional_light = None;
self.pending_mesh_instances_3d.clear();
self.pending_scene_radius = 100.0;
self.current_z = 0.0;
self.vnode_stack.clear();
self.event_handlers.clear();
let current_time = self.current_time();
let resolution = [self.current_width() as f32, self.current_height() as f32];
let time_uniform: [f32; 4] = [
current_time,
resolution[0],
resolution[1],
0.0, ];
self.queue.write_buffer(
&self.volumetric_uniform_buffer,
0,
bytemuck::cast_slice(&time_uniform),
);
self.frame_generation += 1;
const MAX_MEMO_AGE: u64 = 1000;
if self.frame_generation > MAX_MEMO_AGE {
let cutoff = self.frame_generation - MAX_MEMO_AGE;
self.memo_cache.retain(|_, entry| entry.frame_gen >= cutoff);
}
self.last_frame_start = std::time::Instant::now();
self.telemetry.draw_calls = 0;
self.telemetry.vertices = 0;
}
pub fn begin_frame(&mut self, window_id: winit::window::WindowId) -> wgpu::CommandEncoder {
self.begin_frame_internal(window_id, true)
}
pub fn begin_frame_reuse(
&mut self,
window_id: winit::window::WindowId,
) -> wgpu::CommandEncoder {
self.begin_frame_internal(window_id, false)
}
fn begin_frame_internal(
&mut self,
window_id: winit::window::WindowId,
reset_state: bool,
) -> wgpu::CommandEncoder {
if let Some(rx) = &self.ai_material_rx {
while let Ok(res) = rx.try_recv() {
match res {
Ok(_) => tracing::info!("[Surtr] Received AI generated material"),
Err(e) => tracing::warn!("[Surtr] AI material generation error: {:?}", e),
}
}
}
self.staging_belt.recall();
self.current_window = Some(window_id);
if reset_state {
self.reset_frame_state();
}
let ctx = self
.surfaces
.get(&window_id)
.expect("Window not registered");
let time = self.start_time.elapsed().as_secs_f32();
let logical_w = ctx.config.width as f32 / ctx.scale_factor;
let logical_h = ctx.config.height as f32 / ctx.scale_factor;
let dt = time - self.current_scene.time;
self.current_scene.time = time;
self.current_scene.delta_time = dt;
self.current_scene.resolution = [logical_w, logical_h];
self.current_scene.scale_factor = ctx.scale_factor;
self.current_scene.proj =
glam::Mat4::orthographic_lh(0.0, logical_w, logical_h, 0.0, -1000.0, 1000.0);
self.queue.write_buffer(
&self.scene_buffer,
0,
bytemuck::bytes_of(&self.current_scene),
);
self.device
.create_command_encoder(&wgpu::CommandEncoderDescriptor {
label: Some("Surtr Command Encoder"),
})
}
pub fn register_window(&mut self, window: Arc<winit::window::Window>) {
let size = window.inner_size();
let surface = self
.instance
.create_surface(window.clone())
.expect("Failed to create surface");
let caps = surface.get_capabilities(&self.adapter);
let format = caps.formats[0];
let present_mode = if caps.present_modes.contains(&wgpu::PresentMode::Mailbox) {
wgpu::PresentMode::Mailbox
} else {
tracing::warn!("[GPU] Mailbox not supported, falling back to Fifo (V-Sync)");
wgpu::PresentMode::Fifo
};
let alpha_mode = if caps
.alpha_modes
.contains(&wgpu::CompositeAlphaMode::PostMultiplied)
{
wgpu::CompositeAlphaMode::PostMultiplied
} else if caps
.alpha_modes
.contains(&wgpu::CompositeAlphaMode::PreMultiplied)
{
wgpu::CompositeAlphaMode::PreMultiplied
} else {
caps.alpha_modes[0]
};
tracing::info!(
"[GPU] Configuring surface: {}x{} | {:?} | {:?}",
size.width,
size.height,
present_mode,
alpha_mode
);
let config = wgpu::SurfaceConfiguration {
usage: wgpu::TextureUsages::RENDER_ATTACHMENT,
format,
width: size.width,
height: size.height,
present_mode,
alpha_mode,
view_formats: vec![],
desired_maximum_frame_latency: 1,
};
surface.configure(&self.device, &config);
let ctx = create_surface_context(
&self.device,
surface,
config,
&self.env_bind_group_layout,
&self.texture_bind_group_layout,
window.scale_factor() as f32,
self.quality_level.msaa_sample_count(),
&mut self.registry,
);
self.surfaces.insert(window.id(), ctx);
}
pub(crate) fn shatter_internal(
&mut self,
rect: Rect,
pieces: u32,
force: f32,
color: [f32; 4],
material_id: u32,
) {
let count = (pieces as f32).sqrt().ceil() as u32;
let dw = rect.width / count as f32;
let dh = rect.height / count as f32;
let c = self.apply_opacity(color);
let cx = rect.x + rect.width * 0.5;
let cy = rect.y + rect.height * 0.5;
for y in 0..count {
for x in 0..count {
let init_x = rect.x + x as f32 * dw;
let init_y = rect.y + y as f32 * dh;
let dx = (init_x + dw * 0.5) - cx;
let dy = (init_y + dh * 0.5) - cy;
let dist = (dx * dx + dy * dy).sqrt().max(1.0);
let nx = dx / dist;
let ny = dy / dist;
let hash =
((x as f32 * 12.9898 + y as f32 * 78.233).sin().fract() * 43_758.547).fract();
let hash2 =
((x as f32 * 37.11 + y as f32 * 149.87).sin().fract() * 23_412.19).fract();
let speed_var = 0.5 + hash * 1.5;
let angle = ny.atan2(nx) + (hash2 - 0.5) * 0.6;
let disp_x = angle.cos() * force * 50.0 * speed_var;
let disp_y = angle.sin() * force * 50.0 * speed_var;
let gravity = force * force * 20.0;
let scale_factor = (1.0 - (force / 6.0).min(1.0)).max(0.0);
let shard_w = dw * scale_factor;
let shard_h = dh * scale_factor;
let displaced_x = init_x + disp_x + (dw - shard_w) * 0.5;
let displaced_y = init_y + disp_y + gravity + (dh - shard_h) * 0.5;
let shard_rect = Rect {
x: displaced_x,
y: displaced_y,
width: shard_w,
height: shard_h,
};
let uv = Rect {
x: x as f32 / count as f32,
y: y as f32 / count as f32,
width: 1.0 / count as f32,
height: 1.0 / count as f32,
};
self.fill_rect_with_full_params(shard_rect, c, material_id, None, force, uv);
}
}
}
pub(crate) fn recursive_bolt(
&mut self,
from: [f32; 2],
to: [f32; 2],
depth: u32,
color: [f32; 4],
) {
if depth == 0 {
self.draw_lightning_segment(from, to, color);
return;
}
let mid_x = (from[0] + to[0]) * 0.5;
let mid_y = (from[1] + to[1]) * 0.5;
let dx = to[0] - from[0];
let dy = to[1] - from[1];
let len = (dx * dx + dy * dy).sqrt();
if len < 1e-4 {
return;
}
let offset_scale = len * 0.15;
let seed = (from[0] * 12.9898 + from[1] * 78.233 + (depth as f32) * 37.11)
.sin()
.fract();
let offset_x = -dy / len * (seed - 0.5) * offset_scale;
let offset_y = dx / len * (seed - 0.5) * offset_scale;
let mid = [mid_x + offset_x, mid_y + offset_y];
self.recursive_bolt(from, mid, depth - 1, color);
self.recursive_bolt(mid, to, depth - 1, color);
if depth > 2 && seed > 0.8 {
let branch_to = [
mid[0] + offset_x * 2.0 + (seed * 100.0).sin() * 50.0,
mid[1] + offset_y * 2.0 + (seed * 100.0).cos() * 50.0,
];
self.recursive_bolt(mid, branch_to, depth - 2, color);
}
}
pub(crate) fn draw_lightning_segment(&mut self, from: [f32; 2], to: [f32; 2], color: [f32; 4]) {
let dx = to[0] - from[0];
let dy = to[1] - from[1];
let len = (dx * dx + dy * dy).sqrt();
if len < 0.001 {
return;
}
let glow_width = 32.0;
let core_width = 4.0;
let c = self.apply_opacity(color);
let gnx = -dy / len * glow_width * 0.5;
let gny = dx / len * glow_width * 0.5;
let gp1 = [from[0] + gnx, from[1] + gny];
let gp2 = [to[0] + gnx, to[1] + gny];
let gp3 = [to[0] - gnx, to[1] - gny];
let gp4 = [from[0] - gnx, from[1] - gny];
self.push_oriented_quad(
[gp1, gp2, gp3, gp4],
c,
9,
Rect {
x: 0.0,
y: 0.0,
width: 1.0,
height: 1.0,
},
);
let cnx = -dy / len * core_width * 0.5;
let cny = dx / len * core_width * 0.5;
let cp1 = [from[0] + cnx, from[1] + cny];
let cp2 = [to[0] + cnx, to[1] + cny];
let cp3 = [to[0] - cnx, to[1] - cny];
let cp4 = [from[0] - cnx, from[1] - cny];
self.push_oriented_quad(
[cp1, cp2, cp3, cp4],
[1.0, 1.0, 1.0, c[3]],
0,
Rect {
x: 0.0,
y: 0.0,
width: 1.0,
height: 1.0,
},
);
}
pub(crate) fn push_oriented_quad(
&mut self,
points: [[f32; 2]; 4],
color: [f32; 4],
material_id: u32,
uv_rect: Rect,
) {
let scissor = self.clip_stack.last().copied();
let texture_id = None;
let (translation, scale_transform, rotation, _, _) = self.current_transform();
let current_instance_data = InstanceData {
translation,
scale: scale_transform,
rotation,
blur_radius: 0.0,
ior_override: 0.0,
glass_intensity: 1.0,
};
let material =
Self::resolve_material_with_context(material_id, &self.current_draw_material);
let final_material_id = match material {
cvkg_core::DrawMaterial::Opaque => material_id,
cvkg_core::DrawMaterial::TopUI => crate::renderer::material_id::TOP_UI,
cvkg_core::DrawMaterial::Glass { .. } => crate::renderer::material_id::GLASS,
cvkg_core::DrawMaterial::Blend { mode } => 7 + mode,
};
let last_call = self.draw_calls.last();
let needs_new_call = self.draw_calls.is_empty()
|| self.current_texture_id != texture_id
|| last_call.unwrap().scissor_rect != scissor
|| last_call.unwrap().panel_id != self.current_panel_id
|| last_call.unwrap().material != material
|| {
let last_material = last_call.unwrap().material;
matches!((material, last_material),
(cvkg_core::DrawMaterial::Glass { blur_radius: a, ior_override: b, glass_intensity: c },
cvkg_core::DrawMaterial::Glass { blur_radius: d, ior_override: e, glass_intensity: f })
if a != d || b != e || c != f)
};
if needs_new_call {
self.current_texture_id = texture_id;
self.instance_data.push(current_instance_data);
self.draw_calls.push(DrawCall {
target_id: None,
panel_id: self.current_panel_id,
texture_id,
scissor_rect: scissor,
index_start: self.indices.len() as u32,
index_count: 0,
instance_count: 1,
material,
instance_start: (self.instance_data.len() - 1) as u32,
draw_order: 0,
});
} else {
self.instance_data.push(current_instance_data);
if let Some(call) = self.draw_calls.last_mut() {
call.instance_count += 1;
}
}
let uvs = [
[uv_rect.x, uv_rect.y],
[uv_rect.x + uv_rect.width, uv_rect.y],
[uv_rect.x + uv_rect.width, uv_rect.y + uv_rect.height],
[uv_rect.x, uv_rect.y + uv_rect.height],
];
let rect = Rect {
x: points[0][0],
y: points[0][1],
width: 1.0,
height: 1.0,
};
for i in 0..4 {
let px = points[i][0];
let py = points[i][1];
self.vertices.push(Vertex {
position: [px, py, 0.0],
normal: [0.0, 0.0, 1.0],
uv: uvs[i],
color,
material_id: final_material_id,
radius: 0.0,
slice: [0.0, 0.0, 0.0, 1.0],
logical: [px - rect.x, py - rect.y],
size: [rect.width, rect.height],
clip: [-f32::INFINITY, -f32::INFINITY, f32::INFINITY, f32::INFINITY],
tex_index: 0,
});
}
let base = self.vertices.len() as u32 - 4;
self.indices
.extend_from_slice(&[base, base + 1, base + 2, base, base + 2, base + 3]);
if let Some(call) = self.draw_calls.last_mut() {
call.index_count += 6;
}
}
pub(crate) fn get_texture_id(&mut self, name: &str) -> Option<u32> {
self.texture_registry.get(name).copied()
}
pub fn fill_rect_with_mode(
&mut self,
rect: Rect,
color: [f32; 4],
material_id: u32,
texture_id: Option<u32>,
) {
self.fill_rect_with_full_params(
rect,
color,
material_id,
texture_id,
0.0,
Rect {
x: 0.0,
y: 0.0,
width: 1.0,
height: 1.0,
},
);
}
pub(crate) fn fill_rect_with_full_params(
&mut self,
rect: Rect,
color: [f32; 4],
material_id: u32,
texture_id: Option<u32>,
radius: f32,
uv_rect: Rect,
) {
if let Some(shadow) = self.shadow_stack.last().copied()
&& shadow.color[3] > 0.001
{
let shadow_rect = Rect {
x: rect.x + shadow._offset[0],
y: rect.y + shadow._offset[1],
width: rect.width,
height: rect.height,
};
Renderer::draw_drop_shadow(
self,
shadow_rect,
radius,
shadow.color,
shadow.radius,
0.0, );
}
let slice = self
.slice_stack
.last()
.copied()
.map(|(a, o)| [a, o, 1.0, 1.0])
.unwrap_or([0.0, 0.0, 0.0, 1.0]);
self.fill_rect_with_full_params_and_slice(
rect,
color,
material_id,
texture_id,
radius,
uv_rect,
slice,
[0.0, 0.0],
);
}
#[allow(clippy::too_many_arguments)]
pub(crate) fn fill_rect_with_full_params_and_slice(
&mut self,
mut rect: Rect,
color: [f32; 4],
material_id: u32,
texture_id: Option<u32>,
radius: f32,
uv_rect: Rect,
slice: [f32; 4],
_glyph_time: [f32; 2],
) {
if material_id != crate::renderer::material_id::GLASS {
let scale = self.current_scale_factor();
let snap = |v: f32| (v * scale).round() / scale;
rect.x = snap(rect.x);
rect.y = snap(rect.y);
rect.width = snap(rect.width);
rect.height = snap(rect.height);
}
let scissor = self.clip_stack.last().copied();
let material =
Self::resolve_material_with_context(material_id, &self.current_draw_material);
let final_material_id = match material {
cvkg_core::DrawMaterial::Opaque => material_id,
cvkg_core::DrawMaterial::TopUI => crate::renderer::material_id::TOP_UI,
cvkg_core::DrawMaterial::Glass { .. } => crate::renderer::material_id::GLASS,
cvkg_core::DrawMaterial::Blend { mode } => 7 + mode,
};
let (translation, scale_transform, rotation, _, _) = self.current_transform();
let (blur_radius, ior_override, glass_intensity) = if let cvkg_core::DrawMaterial::Glass {
blur_radius,
ior_override,
glass_intensity,
} = material
{
(blur_radius, ior_override, glass_intensity)
} else {
(0.0, 0.0, 1.0)
};
let current_instance_data = InstanceData {
translation,
scale: scale_transform,
rotation,
blur_radius,
ior_override,
glass_intensity,
};
let last_call = self.draw_calls.last();
let needs_new_call = self.draw_calls.is_empty()
|| last_call.unwrap().scissor_rect != scissor
|| last_call.unwrap().material != material
|| last_call.unwrap().texture_id != self.current_texture_id
|| last_call.unwrap().panel_id != self.current_panel_id
|| {
let last_material = last_call.unwrap().material;
matches!((material, last_material),
(cvkg_core::DrawMaterial::Glass { blur_radius: a, ior_override: b, glass_intensity: c },
cvkg_core::DrawMaterial::Glass { blur_radius: d, ior_override: e, glass_intensity: f })
if a != d || b != e || c != f)
};
if needs_new_call {
self.current_texture_id = Some(0); self.instance_data.push(current_instance_data);
self.draw_calls.push(DrawCall {
target_id: None,
panel_id: self.current_panel_id,
texture_id: self.current_texture_id,
scissor_rect: scissor,
index_start: self.indices.len() as u32,
index_count: 0,
instance_count: 1,
material,
instance_start: (self.instance_data.len() - 1) as u32,
draw_order: 0,
});
} else {
self.instance_data.push(current_instance_data);
if let Some(call) = self.draw_calls.last_mut() {
call.instance_count += 1;
}
}
let scale = self.current_scale_factor();
let snap = |v: f32| (v * scale).round() / scale;
let base_idx = self.vertices.len() as u32;
let x1 = snap(rect.x);
let y1 = snap(rect.y);
let x2 = snap(rect.x + rect.width);
let y2 = snap(rect.y + rect.height);
let z = -self.current_z;
let normal = [0.0, 0.0, 1.0];
let clip_rect = self.clip_stack.last().copied().unwrap_or(cvkg_core::Rect {
x: -10000.0,
y: -10000.0,
width: 20000.0,
height: 20000.0,
});
let clip = [clip_rect.x, clip_rect.y, clip_rect.width, clip_rect.height];
let tex_index = texture_id.unwrap_or(0);
self.vertices.push(Vertex {
position: [x1, y1, z],
normal,
uv: [uv_rect.x, uv_rect.y],
color,
material_id: final_material_id,
radius,
slice,
logical: [0.0, 0.0],
size: [rect.width, rect.height],
clip,
tex_index,
});
self.vertices.push(Vertex {
position: [x2, y1, z],
normal,
uv: [uv_rect.x + uv_rect.width, uv_rect.y],
color,
material_id: final_material_id,
radius,
slice,
logical: [rect.width, 0.0],
size: [rect.width, rect.height],
clip,
tex_index,
});
self.vertices.push(Vertex {
position: [x2, y2, z],
normal,
uv: [uv_rect.x + uv_rect.width, uv_rect.y + uv_rect.height],
color,
material_id: final_material_id,
radius,
slice,
logical: [rect.width, rect.height],
size: [rect.width, rect.height],
clip,
tex_index,
});
self.vertices.push(Vertex {
position: [x1, y2, z],
normal,
uv: [uv_rect.x, uv_rect.y + uv_rect.height],
color,
material_id: final_material_id,
radius,
slice,
logical: [0.0, rect.height],
size: [rect.width, rect.height],
clip,
tex_index,
});
self.indices.extend_from_slice(&[
base_idx,
base_idx + 1,
base_idx + 2,
base_idx,
base_idx + 2,
base_idx + 3,
]);
if let Some(call) = self.draw_calls.last_mut() {
call.index_count += 6;
}
}
pub fn end_frame(&mut self, mut encoder: wgpu::CommandEncoder) {
struct ActiveFrameResources {
surface_texture: Option<wgpu::SurfaceTexture>,
target_view: wgpu::TextureView,
scene_texture: wgpu::TextureView,
scene_msaa_texture: wgpu::TextureView,
depth_texture_view: wgpu::TextureView,
blur_env_bind_group_a: wgpu::BindGroup,
blur_env_bind_group_b: wgpu::BindGroup,
bloom_env_bind_group_a: wgpu::BindGroup,
bloom_env_bind_group_b: wgpu::BindGroup,
}
let res = if let Some(window_id) = self.current_window {
let Some(ctx) = self.surfaces.get(&window_id) else {
tracing::error!("[GPU] Missing surface context for end_frame");
return;
};
let frame = match ctx.surface.get_current_texture() {
wgpu::CurrentSurfaceTexture::Success(t) => t,
wgpu::CurrentSurfaceTexture::Suboptimal(t) => {
ctx.surface.configure(&self.device, &ctx.config);
t
}
other => {
tracing::warn!(
"[GPU] Surface texture acquisition failed ({:?}), reconfiguring surface",
other
);
ctx.surface.configure(&self.device, &ctx.config);
match ctx.surface.get_current_texture() {
wgpu::CurrentSurfaceTexture::Success(t) => t,
wgpu::CurrentSurfaceTexture::Suboptimal(t) => {
ctx.surface.configure(&self.device, &ctx.config);
t
}
retry_failed => {
tracing::error!(
"[GPU] Surface texture retry also failed ({:?}), skipping frame",
retry_failed
);
self.queue.submit(std::iter::once(encoder.finish()));
return;
}
}
}
};
let view = frame
.texture
.create_view(&wgpu::TextureViewDescriptor::default());
ActiveFrameResources {
surface_texture: Some(frame),
target_view: view,
scene_texture: ctx.scene_texture.clone(),
scene_msaa_texture: ctx.scene_msaa_texture.clone(),
depth_texture_view: ctx.depth_texture_view.clone(),
blur_env_bind_group_a: ctx.blur_env_bind_group_a.clone(),
blur_env_bind_group_b: ctx.blur_env_bind_group_b.clone(),
bloom_env_bind_group_a: ctx.bloom_env_bind_group_a.clone(),
bloom_env_bind_group_b: ctx.bloom_env_bind_group_b.clone(),
}
} else {
let Some(ctx) = self.headless_context.as_ref() else {
tracing::error!("[GPU] No headless context for end_frame");
return;
};
ActiveFrameResources {
surface_texture: None,
target_view: ctx.output_view.clone(),
scene_texture: ctx.scene_texture.clone(),
scene_msaa_texture: ctx.scene_msaa_texture.clone(),
depth_texture_view: ctx.depth_texture_view.clone(),
blur_env_bind_group_a: ctx.blur_env_bind_group_a.clone(),
blur_env_bind_group_b: ctx.blur_env_bind_group_b.clone(),
bloom_env_bind_group_a: ctx.bloom_env_bind_group_a.clone(),
bloom_env_bind_group_b: ctx.bloom_env_bind_group_b.clone(),
}
};
if !self.frame_rendered && (!self.vertices.is_empty() || !self.indices.is_empty()) {
tracing::debug!(
"[GPU] Auto-flushing staging belt in end_frame (render_frame was not called)"
);
let mut staging_encoder =
self.device
.create_command_encoder(&wgpu::CommandEncoderDescriptor {
label: Some("Surtr Auto-Flush Staging Encoder"),
});
if !self.vertices.is_empty() {
let v_bytes = bytemuck::cast_slice(&self.vertices);
self.staging_belt
.write_buffer(
&mut staging_encoder,
&self.geometry_buffers.vertex_buffer,
0,
wgpu::BufferSize::new(v_bytes.len() as u64).unwrap(),
)
.copy_from_slice(v_bytes);
}
if !self.indices.is_empty() {
let i_bytes = bytemuck::cast_slice(&self.indices);
self.staging_belt
.write_buffer(
&mut staging_encoder,
&self.geometry_buffers.index_buffer,
0,
wgpu::BufferSize::new(i_bytes.len() as u64).unwrap(),
)
.copy_from_slice(i_bytes);
}
if !self.instance_data.is_empty() {
let inst_bytes = bytemuck::cast_slice(&self.instance_data);
self.staging_belt
.write_buffer(
&mut staging_encoder,
&self.geometry_buffers.instance_buffer,
0,
wgpu::BufferSize::new(inst_bytes.len() as u64).unwrap(),
)
.copy_from_slice(inst_bytes);
}
self.staging_belt.finish();
self.staging_command_buffers.push(staging_encoder.finish());
}
let has_glass = self
.draw_calls
.iter()
.any(|c| matches!(c.material, cvkg_core::DrawMaterial::Glass { .. }));
let has_bloom = self.bloom_enabled;
let has_accessibility =
self.color_blind_mode != crate::color_blindness::ColorBlindMode::Normal;
let (blur_id, bloom_id) = if let Some(window_id) = self.current_window {
let ctx = self.surfaces.get(&window_id).unwrap();
(ctx.blur_tex_a, ctx.bloom_tex_a)
} else {
let ctx = self.headless_context.as_ref().unwrap();
(ctx.blur_tex_a, ctx.bloom_tex_a)
};
self.registry
.alias(crate::kvasir::nodes::RES_BLUR_A, blur_id);
self.registry
.alias(crate::kvasir::nodes::RES_BLOOM_A, bloom_id);
self.registry
.alias_view(crate::kvasir::nodes::RES_SCENE, res.scene_texture.clone());
self.registry.alias_view(
crate::kvasir::nodes::RES_SCENE_MSAA,
res.scene_msaa_texture.clone(),
);
let scale = self.current_scale_factor();
let scale_bits = scale.to_bits();
let active_offscreens_count = self.active_offscreens.len();
let portal_regions_count = self.portal_regions.len();
let width = self.current_width();
let height = self.current_height();
let has_volumetric = self.volumetric_enabled;
let mut offscreen_hash: u64 = 0;
for offscreen in &self.active_offscreens {
offscreen_hash = offscreen_hash.wrapping_add(
offscreen.target_id.wrapping_mul(31)
^ (offscreen.blend_mode as u64).wrapping_mul(17),
);
}
let mut portal_hash: u64 = 0;
for region in &self.portal_regions {
portal_hash = portal_hash.wrapping_add(
(region.x.to_bits() as u64)
.wrapping_mul(7)
.wrapping_add((region.y.to_bits() as u64).wrapping_mul(13))
.wrapping_add((region.width.to_bits() as u64).wrapping_mul(19))
.wrapping_add((region.height.to_bits() as u64).wrapping_mul(23)),
);
}
let use_cache = if let Some(ref cached) = self.cached_graph_plan {
cached.matches(
has_glass,
has_bloom,
has_accessibility,
has_volumetric,
active_offscreens_count,
offscreen_hash,
portal_regions_count,
portal_hash,
width,
height,
scale_bits,
self.material_compilation_hash,
)
} else {
false
};
for (id, panel) in &self.world_space_panels {
let width = (panel.world_size.0 * panel.pixels_per_unit).max(1.0) as u32;
let height = (panel.world_size.1 * panel.pixels_per_unit).max(1.0) as u32;
self.registry
.allocate_offscreen(&self.device, *id, [width, height]);
}
self.current_scene.ibl_enabled = if has_glass { 1 } else { 0 };
self.queue.write_buffer(
&self.scene_buffer,
0,
bytemuck::bytes_of(&self.current_scene),
);
if !use_cache {
let render_graph = crate::kvasir::nodes::build_render_graph(
&crate::kvasir::nodes::RenderGraphConfig {
has_glass,
has_bloom,
has_accessibility,
has_ibl: has_glass,
has_volumetric,
active_offscreens: &self.active_offscreens,
portal_regions: &self.portal_regions.iter().cloned().collect::<Vec<_>>(),
world_space_panels: &self.world_space_panels,
width,
height,
scale,
directional_light: self.pending_directional_light,
mesh_instances_3d: std::mem::take(&mut self.pending_mesh_instances_3d),
transparent_meshes_3d: std::mem::take(
&mut self.pending_transparent_instances_3d,
),
cascade_splits: [8.0, 25.0, 70.0, 200.0],
camera_view_proj: self.current_scene.proj * self.current_scene.view,
camera_pos: glam::Vec3::from(self.current_scene.camera_pos),
},
);
let planner = crate::kvasir::planner::ExecutionPlanner::new(&render_graph);
let compiled_plan = match planner.compile() {
Ok(plan) => plan,
Err(e) => {
tracing::error!(
"[Kvasir] Render graph compilation failed ({}), skipping render passes",
e
);
if let Some(surface_texture) = res.surface_texture {
surface_texture.present();
tracing::info!("[Surtr] Frame presented (graph compilation fallback)");
}
return;
}
};
self.cached_graph_plan = Some(crate::kvasir::graph_cache::CachedGraphPlan {
has_glass,
has_bloom,
has_accessibility,
has_volumetric,
active_offscreens_count,
offscreen_content_hash: offscreen_hash,
portal_regions_count,
portal_content_hash: portal_hash,
width,
height,
scale_bits,
material_compilation_hash: self.material_compilation_hash,
graph: render_graph,
plan: compiled_plan,
});
}
let cached = self.cached_graph_plan.as_ref().unwrap();
let frame_start = self.last_frame_start;
let budget_ms = self.frame_budget.target_ms;
let allow_degradation = self.frame_budget.allow_degradation;
for &node_key in &cached.plan {
if allow_degradation && budget_ms > 0.0 {
let elapsed_ms = frame_start.elapsed().as_secs_f32() * 1000.0;
if elapsed_ms > budget_ms
&& let Some(node) = cached.graph.node(node_key)
{
match node.pass_id() {
crate::kvasir::nodes::PassId::BloomExtract
| crate::kvasir::nodes::PassId::BloomBlur
| crate::kvasir::nodes::PassId::Volumetric => {
tracing::trace!(
"[Kvasir] Skipping {} (over budget: {:.1}ms > {:.1}ms)",
node.label(),
elapsed_ms,
budget_ms
);
continue;
}
_ => {} }
}
}
if let Some(node) = cached.graph.node(node_key) {
tracing::trace!("[Kvasir] Executing node: {}", node.label());
let mut ctx = crate::kvasir::node::ExecutionContext {
device: &self.device,
queue: &self.queue,
encoder: &mut encoder,
registry: &self.registry,
renderer: self,
target_view: &res.target_view,
depth_view: &res.depth_texture_view,
blur_env_bind_group_a: &res.blur_env_bind_group_a,
blur_env_bind_group_b: &res.blur_env_bind_group_b,
bloom_env_bind_group_a: &res.bloom_env_bind_group_a,
bloom_env_bind_group_b: &res.bloom_env_bind_group_b,
scale_factor: scale,
};
node.execute(&mut ctx);
}
}
if !self.particles.staging.is_empty() || self.particles.count > 0 {
if !self.particles.staging.is_empty() {
let write_start = self.particles.write_head as usize;
let write_count = self.particles.staging.len();
let max = MAX_PARTICLES;
let effective_count = write_count.min(max);
let drop_count = write_count - effective_count;
let first_chunk = (max - write_start).min(effective_count);
let bytes = bytemuck::cast_slice(
&self.particles.staging[drop_count..drop_count + first_chunk],
);
self.queue.write_buffer(
&self.particle_buffer,
(write_start * std::mem::size_of::<crate::types::GpuParticle>()) as u64,
bytes,
);
if first_chunk < effective_count {
let remaining = effective_count - first_chunk;
let bytes2 = bytemuck::cast_slice(
&self.particles.staging
[drop_count + first_chunk..drop_count + first_chunk + remaining],
);
self.queue.write_buffer(&self.particle_buffer, 0, bytes2);
self.particles.write_head = remaining as u32;
} else {
self.particles.write_head = ((write_start + effective_count) % max) as u32;
}
self.particles.count =
(self.particles.count as usize + effective_count).min(max) as u32;
self.particles.staging.clear();
self.particle_render_bind_group = None;
}
let dt = self.current_scene.delta_time;
let uniforms = crate::types::ParticleUniforms { dt, _pad: [0.0; 7] };
self.queue.write_buffer(
&self.particle_uniform_buffer,
0,
bytemuck::bytes_of(&uniforms),
);
let compute_bind_group = self.device.create_bind_group(&wgpu::BindGroupDescriptor {
label: Some("Particle Compute BG"),
layout: &self.particle_compute_bgl,
entries: &[
wgpu::BindGroupEntry {
binding: 0,
resource: self.particle_buffer.as_entire_binding(),
},
wgpu::BindGroupEntry {
binding: 1,
resource: self.particle_uniform_buffer.as_entire_binding(),
},
],
});
let mut compute_encoder =
self.device
.create_command_encoder(&wgpu::CommandEncoderDescriptor {
label: Some("Particle Compute Encoder"),
});
{
let mut cpass = compute_encoder.begin_compute_pass(&wgpu::ComputePassDescriptor {
label: Some("Particle Integration"),
..Default::default()
});
cpass.set_pipeline(&self.particle_compute_pipeline);
cpass.set_bind_group(0, &compute_bind_group, &[]);
let workgroups = self.particles.count.div_ceil(64).max(1);
cpass.dispatch_workgroups(workgroups, 1, 1);
}
self.staging_command_buffers.push(compute_encoder.finish());
}
if self.particles.count > 0 && self.particles.last_compact.elapsed().as_secs_f32() > 2.0 {
self.particles.last_compact = std::time::Instant::now();
let read_size = (self.particles.count as usize
* std::mem::size_of::<crate::types::GpuParticle>())
as u64;
let staging_buf = self.device.create_buffer(&wgpu::BufferDescriptor {
label: Some("Particle Compact Staging"),
size: read_size,
usage: wgpu::BufferUsages::COPY_DST | wgpu::BufferUsages::MAP_READ,
mapped_at_creation: false,
});
let mut compact_encoder =
self.device
.create_command_encoder(&wgpu::CommandEncoderDescriptor {
label: Some("Particle Compact Copy"),
});
compact_encoder.copy_buffer_to_buffer(
&self.particle_buffer,
0,
&staging_buf,
0,
read_size,
);
self.staging_command_buffers.push(compact_encoder.finish());
}
if self.particles.count > 0 {
if self.particle_render_bind_group.is_none() {
self.particle_render_bind_group =
Some(self.device.create_bind_group(&wgpu::BindGroupDescriptor {
label: Some("Particle Render BG"),
layout: &self.particle_render_bgl,
entries: &[wgpu::BindGroupEntry {
binding: 0,
resource: self.particle_buffer.as_entire_binding(),
}],
}));
}
if let Some(bg) = &self.particle_render_bind_group {
let mut render_encoder =
self.device
.create_command_encoder(&wgpu::CommandEncoderDescriptor {
label: Some("Particle Render Encoder"),
});
{
let mut rpass = render_encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
label: Some("Particle Render"),
color_attachments: &[Some(wgpu::RenderPassColorAttachment {
view: &res.target_view,
resolve_target: None,
ops: wgpu::Operations {
load: wgpu::LoadOp::Load,
store: wgpu::StoreOp::Store,
},
depth_slice: None,
})],
depth_stencil_attachment: None,
timestamp_writes: None,
occlusion_query_set: None,
multiview_mask: None,
});
rpass.set_pipeline(&self.particle_render_pipeline);
rpass.set_bind_group(0, bg, &[]);
rpass.draw(0..self.particles.count, 0..1);
}
self.staging_command_buffers.push(render_encoder.finish());
}
}
self.staging_command_buffers.push(encoder.finish());
if let (Some(q), Some(b), Some(rb)) = (
&self.skuld_queries,
&self.skuld_buffer,
&self.skuld_read_buffer,
) {
let mut resolve_encoder =
self.device
.create_command_encoder(&wgpu::CommandEncoderDescriptor {
label: Some("Skuld Resolve Encoder"),
});
resolve_encoder.resolve_query_set(q, 0..2, b, 0);
resolve_encoder.copy_buffer_to_buffer(b, 0, rb, 0, 16);
self.staging_command_buffers.push(resolve_encoder.finish());
}
let cmds = std::mem::take(&mut self.staging_command_buffers);
self.queue.submit(cmds);
self.telemetry.frame_time_ms = self.last_frame_start.elapsed().as_secs_f32() * 1000.0;
self.update_vram_telemetry();
self.registry.evict_frame_resources();
if let Some(f) = res.surface_texture {
f.present();
tracing::info!("[Surtr] Frame presented");
}
}
pub fn submit_buckets(&mut self, buckets: &cvkg_compositor::CommandBuckets) {
let mut active_offscreens = Vec::new();
let mut current_target_id = None;
let mut sorted_scene: Vec<_> = buckets.scene_commands.iter().collect();
sorted_scene.sort_by_key(|cmd| match cmd {
cvkg_compositor::engine::RenderCommand::Draw(routed) => {
(routed.z_index as i64, routed.draw_order as i64)
}
_ => (0, 0),
});
for cmd in sorted_scene {
match cmd {
cvkg_compositor::engine::RenderCommand::Draw(routed) => {
self.set_material(cvkg_core::DrawMaterial::Opaque);
self.submit_routed(routed, current_target_id);
}
cvkg_compositor::engine::RenderCommand::PushOffscreen {
source_layer,
material,
bounds,
} => {
current_target_id = Some(source_layer.0);
let width = (bounds.width).max(1.0) as u32;
let height = (bounds.height).max(1.0) as u32;
self.registry
.allocate_offscreen(&self.device, source_layer.0, [width, height]);
if let cvkg_compositor::Material::ShaderEffect {
effect_name,
params_json: _,
..
} = material
{
active_offscreens.push(crate::types::OffscreenEffectConfig {
target_id: source_layer.0,
effect: effect_name.clone(),
blend_mode: 0, effect_args: [0.0; 16], });
}
}
cvkg_compositor::engine::RenderCommand::PopOffscreen => {
current_target_id = None;
}
}
}
self.active_offscreens = active_offscreens;
let mut sorted_glass: Vec<_> = buckets.glass_commands.iter().collect();
sorted_glass.sort_by_key(|cmd| match cmd {
cvkg_compositor::engine::RenderCommand::Draw(routed) => {
(routed.z_index as i64, routed.draw_order as i64)
}
_ => (0, 0),
});
for cmd in sorted_glass {
if let cvkg_compositor::engine::RenderCommand::Draw(routed) = cmd {
self.set_material(Self::convert_compositor_material(&routed.material));
self.submit_routed(routed, None);
}
}
let mut sorted_overlay: Vec<_> = buckets.overlay_commands.iter().collect();
sorted_overlay.sort_by_key(|cmd| match cmd {
cvkg_compositor::engine::RenderCommand::Draw(routed) => {
(routed.z_index as i64, routed.draw_order as i64)
}
_ => (0, 0),
});
for cmd in sorted_overlay {
if let cvkg_compositor::engine::RenderCommand::Draw(routed) = cmd {
self.set_material(cvkg_core::DrawMaterial::TopUI);
self.submit_routed(routed, None);
}
}
}
pub(crate) fn submit_routed(
&mut self,
routed: &cvkg_compositor::RoutedDrawCommand,
target_id: Option<u64>,
) {
let cmd = &routed.command;
if cmd.index_count == 0 {
return;
}
let material = Self::convert_compositor_material(&routed.material);
self.draw_calls.push(DrawCall {
texture_id: cmd.texture_id,
scissor_rect: cmd.scissor_rect,
index_start: cmd.index_start,
index_count: cmd.index_count,
instance_count: 1,
material,
target_id,
panel_id: self.current_panel_id,
instance_start: cmd.instance_id,
draw_order: 0,
});
}
pub(crate) fn apply_opacity(&self, mut color: [f32; 4]) -> [f32; 4] {
if let Some(&alpha) = self.opacity_stack.last() {
color[3] *= alpha;
}
color
}
pub(crate) fn resolve_material(material_id: u32) -> cvkg_core::DrawMaterial {
Self::resolve_material_with_context(material_id, &cvkg_core::DrawMaterial::Opaque)
}
pub(crate) fn resolve_material_with_context(
material_id: u32,
current: &cvkg_core::DrawMaterial,
) -> cvkg_core::DrawMaterial {
use crate::renderer::material_id::*;
if matches!(current, cvkg_core::DrawMaterial::TopUI) && material_id != GLASS {
return cvkg_core::DrawMaterial::TopUI;
}
if let cvkg_core::DrawMaterial::Blend { mode } = current
&& material_id == 0
{
return cvkg_core::DrawMaterial::Blend { mode: *mode };
}
match material_id {
GLASS => {
if let cvkg_core::DrawMaterial::Glass {
blur_radius,
ior_override,
glass_intensity,
} = current
{
cvkg_core::DrawMaterial::Glass {
blur_radius: *blur_radius,
ior_override: *ior_override,
glass_intensity: *glass_intensity,
}
} else {
cvkg_core::DrawMaterial::Glass {
blur_radius: 20.0,
ior_override: 0.0,
glass_intensity: 1.0,
}
}
}
TOP_UI => cvkg_core::DrawMaterial::TopUI,
BLEND_START..=BLEND_END => cvkg_core::DrawMaterial::Blend {
mode: (material_id - 7),
},
_ => cvkg_core::DrawMaterial::Opaque,
}
}
pub(crate) fn convert_compositor_material(
mat: &cvkg_compositor::Material,
) -> cvkg_core::DrawMaterial {
match mat {
cvkg_compositor::Material::Glass { blur_radius, .. } => {
cvkg_core::DrawMaterial::Glass {
blur_radius: *blur_radius,
ior_override: 0.0,
glass_intensity: 1.0,
}
}
cvkg_compositor::Material::Overlay => cvkg_core::DrawMaterial::TopUI,
cvkg_compositor::Material::Multiply => cvkg_core::DrawMaterial::Blend { mode: 1 },
cvkg_compositor::Material::Screen => cvkg_core::DrawMaterial::Blend { mode: 2 },
cvkg_compositor::Material::BlendOverlay => cvkg_core::DrawMaterial::Blend { mode: 3 },
cvkg_compositor::Material::Darken => cvkg_core::DrawMaterial::Blend { mode: 4 },
cvkg_compositor::Material::Lighten => cvkg_core::DrawMaterial::Blend { mode: 5 },
cvkg_compositor::Material::ColorDodge => cvkg_core::DrawMaterial::Blend { mode: 6 },
cvkg_compositor::Material::ColorBurn => cvkg_core::DrawMaterial::Blend { mode: 7 },
cvkg_compositor::Material::HardLight => cvkg_core::DrawMaterial::Blend { mode: 8 },
cvkg_compositor::Material::SoftLight => cvkg_core::DrawMaterial::Blend { mode: 9 },
cvkg_compositor::Material::Difference => cvkg_core::DrawMaterial::Blend { mode: 10 },
cvkg_compositor::Material::Exclusion => cvkg_core::DrawMaterial::Blend { mode: 11 },
cvkg_compositor::Material::Hue => cvkg_core::DrawMaterial::Blend { mode: 12 },
cvkg_compositor::Material::Saturation => cvkg_core::DrawMaterial::Blend { mode: 13 },
cvkg_compositor::Material::Color => cvkg_core::DrawMaterial::Blend { mode: 14 },
cvkg_compositor::Material::Luminosity => cvkg_core::DrawMaterial::Blend { mode: 15 },
cvkg_compositor::Material::Opaque => cvkg_core::DrawMaterial::Opaque,
_ => cvkg_core::DrawMaterial::Opaque,
}
}
pub(crate) fn position_vertices(
vertices: &mut [Vertex],
view_box: Rect,
rect: Rect,
material_id: u32,
clip: [f32; 4],
snap: impl Fn(f32) -> f32,
) {
for v in vertices.iter_mut() {
let rel_x = (v.position[0] - view_box.x) / view_box.width;
let rel_y = (v.position[1] - view_box.y) / view_box.height;
v.position[0] = snap(rect.x + rel_x * rect.width);
v.position[1] = snap(rect.y + rel_y * rect.height);
v.position[2] = 0.0; v.logical = [v.position[0], v.position[1]];
v.clip = clip;
v.material_id = material_id;
}
}
pub(crate) fn emit_draw_call(
renderer: &mut GpuRenderer,
material: cvkg_core::DrawMaterial,
texture_id: Option<u32>,
scissor_rect: Rect,
index_count: u32,
base_vertex: u32,
) {
let draw_order = renderer.current_draw_order;
let (translation, scale_transform, rotation, _, _) = renderer.current_transform();
let current_instance_data = InstanceData {
translation,
scale: scale_transform,
rotation,
blur_radius: 0.0,
ior_override: 0.0,
glass_intensity: 1.0,
};
let last_call = renderer.draw_calls.last();
let needs_new_call = renderer.draw_calls.is_empty()
|| renderer.current_texture_id != texture_id
|| last_call.unwrap().scissor_rect != renderer.clip_stack.last().copied()
|| last_call.unwrap().panel_id != renderer.current_panel_id
|| last_call.unwrap().material != material
|| {
let last_material = last_call.unwrap().material;
matches!((material, last_material),
(cvkg_core::DrawMaterial::Glass { blur_radius: a, ior_override: b, glass_intensity: c },
cvkg_core::DrawMaterial::Glass { blur_radius: d, ior_override: e, glass_intensity: f })
if a != d || b != e || c != f)
};
if needs_new_call {
renderer.current_texture_id = texture_id;
renderer.instance_data.push(current_instance_data);
renderer.draw_calls.push(DrawCall {
target_id: None,
panel_id: renderer.current_panel_id,
texture_id,
scissor_rect: renderer.clip_stack.last().copied(),
index_start: (renderer.indices.len() - index_count as usize) as u32,
index_count,
instance_count: 1,
material,
instance_start: (renderer.instance_data.len() - 1) as u32,
draw_order: 0,
});
} else {
renderer.instance_data.push(current_instance_data);
if let Some(call) = renderer.draw_calls.last_mut() {
call.instance_count += 1;
}
}
}
pub async fn capture_frame(&self) -> Result<Vec<u8>, String> {
let ctx = self
.headless_context
.as_ref()
.ok_or("Headless context required for capture")?;
let u32_size = std::mem::size_of::<u32>() as u32;
let width = ctx.width;
let height = ctx.height;
let bytes_per_row = width * u32_size;
let padding = (256 - (bytes_per_row % 256)) % 256;
let padded_bytes_per_row = bytes_per_row + padding;
let output_buffer = self.device.create_buffer(&wgpu::BufferDescriptor {
label: Some("Capture Buffer"),
size: (padded_bytes_per_row as u64 * height as u64),
usage: wgpu::BufferUsages::COPY_DST | wgpu::BufferUsages::MAP_READ,
mapped_at_creation: false,
});
let mut encoder = self
.device
.create_command_encoder(&wgpu::CommandEncoderDescriptor {
label: Some("Capture Encoder"),
});
encoder.copy_texture_to_buffer(
wgpu::TexelCopyTextureInfo {
texture: &ctx.output_texture,
mip_level: 0,
origin: wgpu::Origin3d::ZERO,
aspect: wgpu::TextureAspect::All,
},
wgpu::TexelCopyBufferInfo {
buffer: &output_buffer,
layout: wgpu::TexelCopyBufferLayout {
offset: 0,
bytes_per_row: Some(padded_bytes_per_row),
rows_per_image: Some(height),
},
},
wgpu::Extent3d {
width,
height,
depth_or_array_layers: 1,
},
);
self.queue.submit(Some(encoder.finish()));
let buffer_slice = output_buffer.slice(..);
let (sender, receiver) = futures::channel::oneshot::channel();
buffer_slice.map_async(wgpu::MapMode::Read, move |v| {
let _ = sender.send(v);
});
let _ = self.device.poll(wgpu::PollType::Wait {
submission_index: None,
timeout: None,
});
if let Ok(Ok(_)) = receiver.await {
let data = buffer_slice.get_mapped_range();
let mut result = Vec::with_capacity((width * height * 4) as usize);
for y in 0..height {
let start = (y * padded_bytes_per_row) as usize;
let end = start + bytes_per_row as usize;
result.extend_from_slice(&data[start..end]);
}
tracing::trace!(
"[GPU] capture_frame: data len={}, first 4 bytes={:?}",
data.len(),
&data[0..4.min(data.len())]
);
drop(data);
output_buffer.unmap();
Ok(result)
} else {
Err("Failed to capture frame".to_string())
}
}
fn hash_gradient_stops(stops: &[[f32; 4]]) -> u64 {
use std::hash::{Hash, Hasher};
let mut hasher = std::collections::hash_map::DefaultHasher::new();
for stop in stops {
for v in stop {
v.to_bits().hash(&mut hasher);
}
}
hasher.finish()
}
#[allow(clippy::collapsible_if)]
pub(crate) fn upload_gradient_stops(&mut self, stops: &[[f32; 4]]) {
if stops.is_empty() {
return;
}
let hash = Self::hash_gradient_stops(stops);
if hash == self.gradient_stops_hash {
if let Some((_, _, bg)) = self.gradient_texture_cache.get(&hash) {
self.gradient_bind_group = bg.clone();
return;
}
}
if let Some((_, view, bg)) = self.gradient_texture_cache.get(&hash) {
self.gradient_stop_texture = view.texture().clone();
self.gradient_stop_texture_view = view.clone();
self.gradient_bind_group = bg.clone();
self.gradient_stops_hash = hash;
return;
}
let max_stops = 32u32;
let num_stops = stops.len().min(max_stops as usize) as u32;
let mut data = vec![0u8; (max_stops as usize) * 4];
for (i, stop) in stops.iter().enumerate().take(max_stops as usize) {
let r = (stop[0].clamp(0.0, 1.0) * 255.0).round() as u8;
let g = (stop[1].clamp(0.0, 1.0) * 255.0).round() as u8;
let b = (stop[2].clamp(0.0, 1.0) * 255.0).round() as u8;
let a = (stop[3].clamp(0.0, 1.0) * 255.0).round() as u8;
#[allow(clippy::identity_op)]
{
data[i * 4 + 0] = r;
data[i * 4 + 1] = g;
data[i * 4 + 2] = b;
data[i * 4 + 3] = a;
}
}
let texture = self.device.create_texture(&wgpu::TextureDescriptor {
label: Some("Gradient Stops Texture"),
size: wgpu::Extent3d {
width: max_stops,
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: &[],
});
self.queue.write_texture(
wgpu::TexelCopyTextureInfo {
texture: &texture,
mip_level: 0,
origin: wgpu::Origin3d::ZERO,
aspect: wgpu::TextureAspect::All,
},
&data,
wgpu::TexelCopyBufferLayout {
offset: 0,
bytes_per_row: Some(max_stops * 4),
rows_per_image: Some(1),
},
wgpu::Extent3d {
width: max_stops,
height: 1,
depth_or_array_layers: 1,
},
);
let texture_view = texture.create_view(&wgpu::TextureViewDescriptor::default());
let bind_group = self.device.create_bind_group(&wgpu::BindGroupDescriptor {
layout: &self.gradient_bind_group_layout,
entries: &[
wgpu::BindGroupEntry {
binding: 0,
resource: wgpu::BindingResource::TextureView(&texture_view),
},
wgpu::BindGroupEntry {
binding: 1,
resource: wgpu::BindingResource::Sampler(&self.dummy_sampler),
},
],
label: Some("Gradient Bind Group"),
});
self.gradient_stops_hash = hash;
self.gradient_stop_texture = texture.clone();
self.gradient_stop_texture_view = texture_view.clone();
self.gradient_bind_group = bind_group.clone();
self.gradient_texture_cache
.insert(hash, (texture, texture_view, bind_group));
}
pub fn draw_gradient_multi(
&mut self,
rect: Rect,
stops: &[[f32; 4]],
angle: f32,
is_radial: bool,
) {
if stops.is_empty() {
return;
}
self.upload_gradient_stops(stops);
let num_stops = stops.len().min(32) as f32;
let material_id = if is_radial { 31u32 } else { 30u32 };
let white = [1.0f32, 1.0, 1.0, 1.0];
let slice = [angle, num_stops, 0.0, 1.0];
self.fill_rect_with_full_params_and_slice(
rect,
white,
material_id,
None,
0.0,
Rect {
x: 0.0,
y: 0.0,
width: 1.0,
height: 1.0,
},
slice,
[0.0, 0.0],
);
}
pub fn submit_mesh_3d(
&mut self,
mesh: &cvkg_core::Mesh,
material: &cvkg_core::Material3D,
transform: &cvkg_core::Transform3D,
) {
let model_matrix = transform.to_matrix();
let mut mesh_vertices: Vec<Vertex3D> = Vec::with_capacity(mesh.vertices.len());
for (i, pos) in mesh.vertices.iter().enumerate() {
let raw_uv = mesh.tex_coords.get(i).copied().unwrap_or([0.0, 0.0]);
let uv = [
raw_uv[0] * material.uv_scale[0] + material.uv_offset[0],
raw_uv[1] * material.uv_scale[1] + material.uv_offset[1],
];
mesh_vertices.push(Vertex3D {
position: *pos,
normal: mesh.normals.get(i).copied().unwrap_or([0.0, 0.0, 1.0]),
uv,
color: material.base_color,
tangent: mesh
.tangents
.get(i)
.copied()
.unwrap_or([0.0, 0.0, 1.0, 1.0]),
});
}
let vertex_bytes: Vec<u8> = bytemuck::cast_slice(&mesh_vertices).to_vec();
let vertex_buffer = self.device.create_buffer(&wgpu::BufferDescriptor {
label: Some("Mesh3D Vertex Buffer"),
size: (mesh_vertices.len() * std::mem::size_of::<Vertex3D>()) as u64,
usage: wgpu::BufferUsages::VERTEX | wgpu::BufferUsages::COPY_DST,
mapped_at_creation: false,
});
let index_bytes: Vec<u8> = bytemuck::cast_slice(&mesh.indices).to_vec();
let index_buffer = self.device.create_buffer(&wgpu::BufferDescriptor {
label: Some("Mesh3D Index Buffer"),
size: (mesh.indices.len() * std::mem::size_of::<u32>()) as u64,
usage: wgpu::BufferUsages::INDEX | wgpu::BufferUsages::COPY_DST,
mapped_at_creation: false,
});
self.queue.write_buffer(&vertex_buffer, 0, &vertex_bytes);
self.queue.write_buffer(&index_buffer, 0, &index_bytes);
let (center, half_extents) = mesh.aabb();
let mesh_radius = half_extents.length().max(1.0);
if mesh_radius > self.pending_scene_radius {
self.pending_scene_radius = mesh_radius;
}
let view_depth = (0..mesh.vertices.len())
.map(|i| {
let world_pos = model_matrix.transform_point3(glam::Vec3::from(mesh.vertices[i]));
(glam::Vec3::from(self.current_scene.camera_pos) - world_pos).length()
})
.sum::<f32>()
/ mesh.vertices.len().max(1) as f32;
let row0 = model_matrix.row(0);
let row1 = model_matrix.row(1);
let row2 = model_matrix.row(2);
let instance_index = self.instance_data_3d.len() as u32;
self.instance_data_3d.push(InstanceData3D {
model_row0: [row0.x, row0.y, row0.z, row0.w],
model_row1: [row1.x, row1.y, row1.z, row1.w],
model_row2: [row2.x, row2.y, row2.z, row2.w],
material_overrides: [material.metallic, material.roughness, 0.0, material.opacity],
uv_scale: material.uv_scale,
uv_offset: material.uv_offset,
});
let gpu_mesh = crate::passes::shadow::GpuMesh3d {
vertex_buffer,
index_buffer,
index_count: mesh.indices.len() as u32,
transform: model_matrix,
view_depth,
instance_index,
};
if material.opacity < 1.0 {
self.pending_transparent_instances_3d.push(gpu_mesh);
} else {
self.pending_mesh_instances_3d.push(gpu_mesh);
}
if self.pending_directional_light.is_none() {
self.pending_directional_light = Some(crate::passes::shadow::DirectionalLight {
direction: glam::Vec3::new(0.5, 0.8, 0.6),
color: glam::Vec3::new(1.0, 0.95, 0.9),
intensity: 1.0,
});
}
}
}