use std::sync::Mutex;
use std::time::Instant;
use crate::Context;
use crate::DrawCommand;
use crate::ShaderOpts;
use crate::image_raw::InstanceData;
use crate::pt::Pt;
use super::Graphics;
use super::core::{ResolvedDraw};
use super::profile::{PROFILE_RENDER, PROFILE_STATS, RenderProfileStats};
impl Graphics {
pub(super) fn resolve_drawables(
&mut self,
drawables: &[DrawCommand],
logical_w: u32,
logical_h: u32,
) {
self.resolved_draws.clear();
let viewport_rect = [0.0, 0.0, logical_w as f32, logical_h as f32];
for drawable in drawables {
match drawable {
DrawCommand::Image(id, opts, shader_id, shader_opts, _) => {
if let Some(Some(entry)) = self.images.get(*id as usize) {
if !entry.visible || !entry.is_ready() {
continue;
}
self.resolved_draws.push(ResolvedDraw {
img_entry: entry.clone(),
opts: *opts,
shader_id: *shader_id,
shader_opts: *shader_opts,
});
}
}
DrawCommand::Text(text, opts) => {
if let Err(e) = self.layout_and_queue_text(text, opts, viewport_rect) {
eprintln!("[spot] Text layout error: {:?}", e);
}
}
}
}
}
pub(super) fn render_batches<'a>(
&'a mut self,
rpass: &mut wgpu::RenderPass<'a>,
screen_size_data: [f32; 4],
sf: f64,
) {
let mut current_opacity = 1.0f32;
let engine_globals = crate::image_raw::EngineGlobals {
screen: screen_size_data,
opacity: current_opacity,
shader_opacity: 1.0, _padding: [0.0; 2],
};
let mut current_engine_globals_offset = self
.image_renderer
.upload_engine_globals(&self.queue, &engine_globals)
.unwrap_or(0);
let default_user_globals = ShaderOpts::default();
let mut current_user_globals_offset = self
.image_renderer
.upload_user_globals_bytes(&self.queue, default_user_globals.as_bytes())
.unwrap_or(0);
self.batch.clear();
let mut current_atlas_index: Option<u32> = None;
let mut current_shader_id: u32 = 0;
let mut current_user_globals = ShaderOpts::default();
let mut current_clip: Option<[Pt; 4]> = None;
let config_width = self.config.width;
let config_height = self.config.height;
rpass.set_scissor_rect(0, 0, config_width.max(1), config_height.max(1));
let mut last_set_scissor: Option<(u32, u32, u32, u32)> = None;
for resolved in &self.resolved_draws {
let img_entry = &resolved.img_entry;
let opts = resolved.opts;
let shader_id = resolved.shader_id;
let shader_opts = resolved.shader_opts;
let draw_opacity = opts.opacity();
let uv_rect = match img_entry.uv_rect {
Some(uv) => uv,
None => continue,
};
let effective_user_globals = shader_opts;
let state_changed = current_atlas_index != img_entry.atlas_index
|| current_shader_id != shader_id
|| current_user_globals != effective_user_globals
|| current_clip != opts.get_clip()
|| current_opacity != draw_opacity;
if state_changed && !self.batch.is_empty() {
let ai = current_atlas_index
.expect("current_atlas_index should be Some if batch is not empty");
let atlas_bg = &self.atlases.get(ai as usize).expect("atlas").bind_group;
if let Ok(range) = self
.image_renderer
.upload_instances(&self.queue, self.batch.as_slice())
{
let pipeline = if current_shader_id == 0 {
&self.default_pipeline
} else {
self.image_pipelines.get(¤t_shader_id).unwrap()
};
self.image_renderer.draw_batch(
rpass,
pipeline,
atlas_bg,
range,
current_user_globals_offset,
current_engine_globals_offset,
);
}
self.batch.clear();
}
if current_opacity != draw_opacity
|| current_user_globals.opacity != resolved.shader_opts.opacity
{
current_opacity = draw_opacity;
let eg = crate::image_raw::EngineGlobals {
screen: screen_size_data,
opacity: current_opacity,
shader_opacity: resolved.shader_opts.opacity,
_padding: [0.0; 2],
};
current_engine_globals_offset = self
.image_renderer
.upload_engine_globals(&self.queue, &eg)
.unwrap_or(0);
}
if current_user_globals != effective_user_globals
|| (current_atlas_index.is_none() && self.batch.is_empty())
{
current_user_globals = effective_user_globals;
current_user_globals_offset = self
.image_renderer
.upload_user_globals_bytes(&self.queue, current_user_globals.as_bytes())
.unwrap_or(current_user_globals_offset);
}
if current_clip != opts.get_clip() {
current_clip = opts.get_clip();
let (sx, sy, sw, sh) = if let Some(clip) = current_clip {
let x0 = (clip[0].as_f32() * sf as f32).clamp(0.0, config_width as f32);
let y0 = (clip[1].as_f32() * sf as f32).clamp(0.0, config_height as f32);
let x1 = ((clip[0].as_f32() + clip[2].as_f32()) * sf as f32)
.clamp(0.0, config_width as f32);
let y1 = ((clip[1].as_f32() + clip[3].as_f32()) * sf as f32)
.clamp(0.0, config_height as f32);
let fw = (x1 - x0).max(0.0) as u32;
let fh = (y1 - y0).max(0.0) as u32;
if fw > 0 && fh > 0 {
(x0 as u32, y0 as u32, fw, fh)
} else {
(0, 0, 1, 1)
}
} else {
(0, 0, config_width, config_height)
};
if last_set_scissor != Some((sx, sy, sw, sh)) {
rpass.set_scissor_rect(sx, sy, sw, sh);
last_set_scissor = Some((sx, sy, sw, sh));
}
}
current_atlas_index = img_entry.atlas_index;
current_shader_id = shader_id;
self.batch.push(InstanceData {
pos: [opts.position()[0].as_f32(), opts.position()[1].as_f32()],
rotation: opts.rotation(),
size: [
img_entry.bounds.width.as_f32() * opts.scale()[0],
img_entry.bounds.height.as_f32() * opts.scale()[1],
],
uv_rect,
});
}
if !self.batch.is_empty() {
let ai = current_atlas_index
.expect("current_atlas_index should be Some if batch is not empty");
let atlas_bg = &self.atlases.get(ai as usize).expect("atlas").bind_group;
if let Ok(range) = self
.image_renderer
.upload_instances(&self.queue, self.batch.as_slice())
{
let pipeline = if current_shader_id == 0 {
&self.default_pipeline
} else {
self.image_pipelines.get(¤t_shader_id).unwrap()
};
self.image_renderer.draw_batch(
rpass,
pipeline,
atlas_bg,
range,
current_user_globals_offset,
current_engine_globals_offset,
);
}
self.batch.clear();
}
}
pub(super) fn render_3d_with_context<'a>(
&'a mut self,
rpass: &mut wgpu::RenderPass<'a>,
context: &Context,
) {
self.model_renderer.begin_frame();
let config_width = self.config.width as f32;
let config_height = self.config.height as f32;
let aspect = config_width / config_height;
let proj = crate::graphics::model_raw::create_perspective(aspect, std::f32::consts::PI / 4.0, 0.1, 1000.0);
self.model_renderer.upload_scene_globals(&self.queue, &self.scene_globals);
let view = crate::graphics::model_raw::create_translation([0.0, 0.0, -5.0]);
for command in context.draw_list_3d() {
match command {
crate::drawable::DrawCommand3D::Model(model, opts, shader_id, shader_opts, skin_id_cmd) => {
if let Some(Some(mesh)) = self.models.get(model.id as usize) {
let model_mat = crate::graphics::model_raw::create_translation(opts.position);
let rot_mat = crate::graphics::model_raw::create_rotation(opts.rotation);
let get_tex_info = |img_id: Option<u32>, fallback_id: u32| -> (&wgpu::TextureView, [f32; 4]) {
let id = img_id.filter(|&id| self.images.get(id as usize).map(|v| v.is_some()).unwrap_or(false)).unwrap_or(fallback_id);
let entry = self.images[id as usize].as_ref().unwrap();
let ai = entry.atlas_index.unwrap_or(0);
let view = &self.atlases[ai as usize].texture.0.view;
let uv = entry.uv_rect.unwrap_or([0.0, 0.0, 1.0, 1.0]);
(view, uv)
};
let (albedo_view, albedo_uv) = get_tex_info(model.material.albedo, self.white_image_id);
let (pbr_view, pbr_uv) = get_tex_info(model.material.pbr, self.black_image_id);
let (normal_view, normal_uv) = get_tex_info(model.material.normal, self.normal_image_id);
let (ao_view, ao_uv) = get_tex_info(model.material.occlusion, self.white_image_id);
let (emissive_view, emissive_uv) = get_tex_info(model.material.emissive, self.black_image_id);
let model_mat_all = crate::graphics::model_raw::multiply(model_mat, rot_mat);
let mvp = crate::graphics::model_raw::multiply(proj, crate::graphics::model_raw::multiply(view, model_mat_all));
let globals = crate::graphics::model_raw::ModelGlobals {
mvp,
model: model_mat_all,
extra: [opts.opacity, 0.0, 0.0, 0.0],
albedo_uv,
pbr_uv,
normal_uv,
ao_uv,
emissive_uv,
};
if let Ok(offset) = self.model_renderer.upload_globals(&self.queue, &globals) {
let pipeline = if *shader_id == 0 {
&self.model_pipeline
} else {
self.model_pipelines.get(shader_id).unwrap_or(&self.model_pipeline)
};
rpass.set_pipeline(pipeline);
rpass.set_vertex_buffer(0, mesh.vertex_buffer.slice(..));
rpass.set_index_buffer(mesh.index_buffer.slice(..), wgpu::IndexFormat::Uint32);
rpass.set_bind_group(0, &self.model_renderer.model_globals_bind_group, &[offset]);
let tex_bg = self.model_renderer.create_texture_bind_group(
&self.device,
albedo_view,
pbr_view,
normal_view,
ao_view,
emissive_view
);
rpass.set_bind_group(1, &tex_bg, &[]);
if let Ok(opts_offset) = self.model_renderer.upload_shader_opts_bytes(&self.queue, shader_opts.as_bytes()) {
rpass.set_bind_group(2, &self.model_renderer.user_shader_opts_bind_group, &[opts_offset]);
}
let mut bone_offset = 0;
if let Some(skin_id) = skin_id_cmd {
if let Some(Some(skin)) = self.skins.get(*skin_id as usize) {
if let Ok(off) = self.model_renderer.upload_bone_matrices(&self.queue, &skin.bone_matrices) {
bone_offset = off;
}
}
}
rpass.set_bind_group(3, &self.model_renderer.bone_matrices_bind_group, &[bone_offset]);
rpass.set_bind_group(4, &self.model_renderer.scene_globals_bind_group, &[]);
rpass.draw_indexed(0..mesh.index_count, 0, 0..1);
}
}
}
crate::drawable::DrawCommand3D::ModelInstanced(model, opts, _shader_id, shader_opts, skin_id_cmd, instances) => {
if instances.is_empty() { continue; }
if let Some(Some(mesh)) = self.models.get(model.id as usize) {
let model_mat = crate::graphics::model_raw::create_translation(opts.position);
let rot_mat = crate::graphics::model_raw::create_rotation(opts.rotation);
let get_tex_info = |img_id: Option<u32>, fallback_id: u32| -> (&wgpu::TextureView, [f32; 4]) {
let id = img_id.filter(|&id| self.images.get(id as usize).map(|v| v.is_some()).unwrap_or(false)).unwrap_or(fallback_id);
let entry = self.images[id as usize].as_ref().unwrap();
let ai = entry.atlas_index.unwrap_or(0);
let view = &self.atlases[ai as usize].texture.0.view;
let uv = entry.uv_rect.unwrap_or([0.0, 0.0, 1.0, 1.0]);
(view, uv)
};
let (albedo_view, albedo_uv) = get_tex_info(model.material.albedo, self.white_image_id);
let (pbr_view, pbr_uv) = get_tex_info(model.material.pbr, self.black_image_id);
let (normal_view, normal_uv) = get_tex_info(model.material.normal, self.normal_image_id);
let (ao_view, ao_uv) = get_tex_info(model.material.occlusion, self.white_image_id);
let (emissive_view, emissive_uv) = get_tex_info(model.material.emissive, self.black_image_id);
let model_mat_all = crate::graphics::model_raw::multiply(model_mat, rot_mat);
let mvp = crate::graphics::model_raw::multiply(proj, crate::graphics::model_raw::multiply(view, model_mat_all));
let globals = crate::graphics::model_raw::ModelGlobals {
mvp,
model: model_mat_all,
extra: [opts.opacity, 0.0, 0.0, 0.0],
albedo_uv,
pbr_uv,
normal_uv,
ao_uv,
emissive_uv,
};
if let Ok(offset) = self.model_renderer.upload_globals(&self.queue, &globals) {
let instance_bytes = bytemuck::cast_slice(&instances);
let instance_buffer = self.device.create_buffer(&wgpu::BufferDescriptor {
label: Some("instancing_buffer"),
size: instance_bytes.len() as u64,
usage: wgpu::BufferUsages::VERTEX | wgpu::BufferUsages::COPY_DST,
mapped_at_creation: false,
});
self.queue.write_buffer(&instance_buffer, 0, instance_bytes);
rpass.set_pipeline(&self.instanced_model_pipeline);
rpass.set_vertex_buffer(0, mesh.vertex_buffer.slice(..));
rpass.set_vertex_buffer(1, instance_buffer.slice(..));
rpass.set_index_buffer(mesh.index_buffer.slice(..), wgpu::IndexFormat::Uint32);
rpass.set_bind_group(0, &self.model_renderer.model_globals_bind_group, &[offset]);
let tex_bg = self.model_renderer.create_texture_bind_group(&self.device, albedo_view, pbr_view, normal_view, ao_view, emissive_view);
rpass.set_bind_group(1, &tex_bg, &[]);
if let Ok(opts_offset) = self.model_renderer.upload_shader_opts_bytes(&self.queue, shader_opts.as_bytes()) {
rpass.set_bind_group(2, &self.model_renderer.user_shader_opts_bind_group, &[opts_offset]);
}
let mut bone_offset = 0;
if let Some(skin_id) = skin_id_cmd {
if let Some(Some(skin)) = self.skins.get(*skin_id as usize) {
if let Ok(off) = self.model_renderer.upload_bone_matrices(&self.queue, &skin.bone_matrices) {
bone_offset = off;
}
}
}
rpass.set_bind_group(3, &self.model_renderer.bone_matrices_bind_group, &[bone_offset]);
rpass.set_bind_group(4, &self.model_renderer.scene_globals_bind_group, &[]);
rpass.draw_indexed(0..mesh.index_count, 0, 0..instances.len() as u32);
}
}
}
}
}
}
pub fn draw_context(
&mut self,
surface: &wgpu::Surface<'_>,
context: &Context,
) -> Result<(), wgpu::SurfaceError> {
let _ = self.process_registrations();
self.draw_drawables_with_context(
surface,
context.draw_list(),
context.scale_factor(),
context,
)
}
fn draw_drawables_with_context(
&mut self,
surface: &wgpu::Surface<'_>,
drawables: &[DrawCommand],
scale_factor: f64,
context: &Context,
) -> Result<(), wgpu::SurfaceError> {
let (lw, lh) = context.window_logical_size();
let sf = if scale_factor.is_finite() && scale_factor > 0.0 {
scale_factor
} else {
1.0
};
let expected_w = ((lw.as_f32() as f64) * sf).round().max(1.0) as u32;
let expected_h = ((lh.as_f32() as f64) * sf).round().max(1.0) as u32;
if expected_w != self.config.width || expected_h != self.config.height {
self.resize(surface, expected_w, expected_h);
}
self.draw_drawables_internal(surface, drawables, sf, Some(context))
}
fn draw_drawables_internal(
&mut self,
surface: &wgpu::Surface<'_>,
drawables: &[DrawCommand],
scale_factor: f64,
_context: Option<&Context>,
) -> Result<(), wgpu::SurfaceError> {
let profile_enabled = *PROFILE_RENDER.get_or_init(|| {
std::env::var("SPOT_PROFILE_RENDER")
.ok()
.map(|v| {
let v = v.trim().to_ascii_lowercase();
!v.is_empty() && v != "0" && v != "false" && v != "off"
})
.unwrap_or(false)
});
let mut t_prev = if profile_enabled {
Some(Instant::now())
} else {
None
};
let frame = surface.get_current_texture()?;
let dt_acquire_ms = if let Some(t0) = t_prev {
t0.elapsed().as_secs_f64() * 1000.0
} else {
0.0
};
t_prev = if profile_enabled {
Some(Instant::now())
} else {
None
};
let view = frame
.texture
.create_view(&wgpu::TextureViewDescriptor::default());
let mut encoder = self
.device
.create_command_encoder(&wgpu::CommandEncoderDescriptor {
label: Some("graphics_encoder"),
});
let dt_encoder_ms = if let Some(t0) = t_prev {
t0.elapsed().as_secs_f64() * 1000.0
} else {
0.0
};
t_prev = if profile_enabled {
Some(Instant::now())
} else {
None
};
self.image_renderer.begin_frame();
let sf = if scale_factor.is_finite() && scale_factor > 0.0 {
scale_factor
} else {
1.0
};
let logical_w = ((self.config.width as f64) / sf).round().max(1.0) as u32;
let logical_h = ((self.config.height as f64) / sf).round().max(1.0) as u32;
let (sw, sh) = (logical_w as f32, logical_h as f32);
let sw_inv = 1.0 / sw;
let sh_inv = 1.0 / sh;
let screen_size_data = [sw_inv * 2.0, sh_inv * 2.0, sw_inv, sh_inv];
self.resolve_drawables(drawables, logical_w, logical_h);
let dt_setup_ms = if let Some(t0) = t_prev {
t0.elapsed().as_secs_f64() * 1000.0
} else {
0.0
};
t_prev = if profile_enabled {
Some(Instant::now())
} else {
None
};
{
let mut rpass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
label: Some("graphics_render_pass_3d"),
color_attachments: &[Some(wgpu::RenderPassColorAttachment {
view: &view,
resolve_target: None,
depth_slice: None,
ops: wgpu::Operations {
load: wgpu::LoadOp::Clear(wgpu::Color::BLACK),
store: wgpu::StoreOp::Store,
},
})],
depth_stencil_attachment: Some(wgpu::RenderPassDepthStencilAttachment {
view: &self.depth_view,
depth_ops: Some(wgpu::Operations {
load: wgpu::LoadOp::Clear(1.0),
store: wgpu::StoreOp::Store,
}),
stencil_ops: None,
}),
timestamp_writes: None,
occlusion_query_set: None,
multiview_mask: None,
});
if let Some(ctx) = _context {
self.render_3d_with_context(&mut rpass, ctx);
}
}
{
let mut rpass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
label: Some("graphics_render_pass_2d"),
color_attachments: &[Some(wgpu::RenderPassColorAttachment {
view: &view,
resolve_target: None,
depth_slice: None,
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,
});
self.render_batches(&mut rpass, screen_size_data, sf);
}
let dt_renderpass_ms = if let Some(t0) = t_prev {
t0.elapsed().as_secs_f64() * 1000.0
} else {
0.0
};
t_prev = if profile_enabled {
Some(Instant::now())
} else {
None
};
self.queue.submit(Some(encoder.finish()));
let dt_submit_ms = if let Some(t0) = t_prev {
t0.elapsed().as_secs_f64() * 1000.0
} else {
0.0
};
frame.present();
if profile_enabled {
let total_ms =
dt_acquire_ms + dt_encoder_ms + dt_setup_ms + dt_renderpass_ms + dt_submit_ms;
let wait_ms = dt_acquire_ms;
let work_ms = total_ms - wait_ms;
let stats_lock =
PROFILE_STATS.get_or_init(|| Mutex::new(RenderProfileStats::default()));
if let Ok(mut s) = stats_lock.lock() {
s.frame = s.frame.saturating_add(1);
s.sum_total_ms += total_ms;
s.sum_wait_ms += wait_ms;
s.sum_work_ms += work_ms;
s.min_total_ms = s.min_total_ms.min(total_ms);
s.max_total_ms = s.max_total_ms.max(total_ms);
if s.frame % 30 == 0 {
let n = s.frame as f64;
eprintln!(
"[spot][render][avg@{}] total={:.3}ms work={:.3} wait={:.3} min={:.3} max={:.3}",
s.frame,
s.sum_total_ms / n,
s.sum_work_ms / n,
s.sum_wait_ms / n,
s.min_total_ms,
s.max_total_ms
);
}
}
}
Ok(())
}
}