Multithreaded render

This commit is contained in:
2025-07-04 21:31:11 +01:00
parent ce78812dd3
commit 1a200ea65e
7 changed files with 689 additions and 642 deletions
Generated
+11 -3
View File
@@ -681,12 +681,10 @@ dependencies = [
"foldhash",
"glam",
"log",
"num_cpus",
"obj",
"rand 0.9.1",
"rayon",
"rspirv",
"serde",
"serde_json",
"simplelog",
"utf-8",
"vulkano",
@@ -1332,6 +1330,16 @@ dependencies = [
"autocfg",
]
[[package]]
name = "num_cpus"
version = "1.17.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "91df4bbde75afed763b708b7eee1e8e7651e02d97f6d5dd763e89367e957b23b"
dependencies = [
"hermit-abi",
"libc",
]
[[package]]
name = "num_enum"
version = "0.7.4"
+4 -4
View File
@@ -24,13 +24,11 @@ egui = "0.31.1"
egui_winit_vulkano = "0.28.0"
egui_plot = "0.32.1"
serde = { version = "1", features = ["derive"] }
serde_json = "1"
#serde = { version = "1", features = ["derive"] }
#serde_json = "1"
utf-8 = "0.7"
rayon = "1.7"
rand = "0.9.1"
foldhash = "*"
@@ -39,6 +37,8 @@ simplelog = "0.12"
rspirv = "0.12"
num_cpus = "1"
# using latest gits
[patch.crates-io]
#vulkano = { git = "https://github.com/vulkano-rs/vulkano" }
+7 -14
View File
@@ -1,12 +1,10 @@
use std::sync::{Arc, RwLock};
use egui::{Color32, Frame, Id};
use egui_plot::{Line, Plot, PlotPoints};
use egui_winit_vulkano::Gui;
use glam::Vec3;
use crate::{
create_csg,
objects::{CSG, Light, Mesh},
};
use crate::objects::{CSG, Light, Mesh};
fn sized_text(ui: &mut egui::Ui, text: impl Into<String>, size: f32) {
ui.label(egui::RichText::new(text).size(size));
@@ -29,7 +27,7 @@ pub(crate) struct GState {
pub(crate) meshes: Vec<Mesh>,
pub(crate) lights: Vec<Light>,
pub(crate) csg: Vec<CSG>,
pub(crate) csg: Vec<Arc<RwLock<CSG>>>,
pub(crate) fps: [f64; 128],
@@ -53,7 +51,7 @@ impl Default for GState {
}
}
pub(crate) fn gui_up(gui: &mut Gui, state: &mut GState) {
pub(crate) fn gui_up(gui: &mut Gui, state: &mut GState, new_csg_needed: &mut bool) {
gui.immediate_ui(|gui| {
let ctx = gui.context();
egui::SidePanel::left(Id::new("main_left"))
@@ -118,6 +116,7 @@ pub(crate) fn gui_up(gui: &mut Gui, state: &mut GState) {
ui.heading("Implicit Surfaces");
let mut csgdel = vec![];
for csg in &mut state.csg {
let mut csg = csg.write().unwrap();
ui.label(csg.name.clone());
csgdel.push(ui.small_button("remove csg").clicked());
ui.add(
@@ -153,13 +152,7 @@ pub(crate) fn gui_up(gui: &mut Gui, state: &mut GState) {
state.csg.remove(i);
}
if state.csg.len() < 32 && ui.small_button("add csg").clicked() {
state.csg.push(CSG {
name: "generated".to_string(),
parts: create_csg(),
pos: Vec3::ZERO,
rot: Vec3::ZERO,
scale: Vec3::ONE,
});
*new_csg_needed = true;
}
ui.heading("Lights");
let mut lightdel = vec![];
+190 -615
View File
File diff suppressed because it is too large Load Diff
+8 -6
View File
@@ -8,7 +8,7 @@ use vulkano::{
memory::allocator::{
AllocationCreateInfo, MemoryAllocatePreference, MemoryTypeFilter, StandardMemoryAllocator,
},
pipeline::graphics::vertex_input::Vertex,
pipeline::{GraphicsPipeline, graphics::vertex_input::Vertex},
};
use crate::ssa::SSATape;
@@ -36,11 +36,13 @@ pub(crate) struct Mesh {
#[derive(Debug)]
pub(crate) struct CSG {
pub(crate) name: String,
pub(crate) parts: SSATape,
pub(crate) pos: Vec3,
pub(crate) rot: Vec3,
pub(crate) scale: Vec3,
pub(crate) name: String,
pub(crate) parts: SSATape,
pub(crate) pos: Vec3,
pub(crate) rot: Vec3,
pub(crate) scale: Vec3,
pub(crate) deferred_pipeline: Arc<GraphicsPipeline>,
pub(crate) normals_pipeline: Arc<GraphicsPipeline>,
}
pub(crate) fn load_obj(
+2
View File
@@ -1,3 +1,5 @@
use std::sync::Arc;
use egui::ahash::HashMapExt;
use foldhash::HashMap;
use rspirv::{dr::Module, spirv};
+467
View File
@@ -0,0 +1,467 @@
use std::{
fs::File,
io::Write,
simd::{StdFloat, cmp::SimdPartialOrd},
sync::{Arc, RwLock, mpmc, mpsc},
time::Instant,
};
use foldhash::HashSet;
use glam::{EulerRot, Mat4, Vec3};
use log::info;
use rspirv::{binary::Assemble, dr::Module};
use vulkano::{
device::Device,
pipeline::{
DynamicState, GraphicsPipeline, PipelineCreateFlags, PipelineLayout,
PipelineShaderStageCreateInfo,
cache::PipelineCache,
graphics::{
GraphicsPipelineCreateInfo,
color_blend::{ColorBlendAttachmentState, ColorBlendState},
depth_stencil::{DepthState, DepthStencilState},
input_assembly::InputAssemblyState,
multisample::MultisampleState,
rasterization::{CullMode, FrontFace, PolygonMode, RasterizationState},
vertex_input::{Vertex, VertexDefinition, VertexInputState},
},
layout::PipelineDescriptorSetLayoutCreateInfo,
},
render_pass::{RenderPass, Subpass},
shader::{ShaderModule, ShaderModuleCreateInfo},
};
use crate::{
DUMP_SPV_TO_FILE, IVertex, ShaderModules, get_spec_constants,
gui::PreviousDebug,
implicit_vs::PushConstantData,
interpreter::{self, IntervalInterpreter, PointInterpreter, VALUE_0},
objects::CSG,
ssa::{SSAInput, SSAOpcode, SSAOpcodeSized, SSATape},
types::Interval,
};
#[derive(Debug)]
pub enum WorkItem {
CreateCSG(
Arc<Device>,
Module,
Module,
Arc<RenderPass>,
Arc<PipelineCache>,
ShaderModules,
PreviousDebug,
isize,
),
GetPushConstants(Arc<RwLock<CSG>>, f32, usize),
}
#[derive(Debug)]
pub enum WorkComplete {
CreateCSG(Arc<RwLock<CSG>>, isize),
GetPushConstants(PushConstantData, usize),
}
pub fn thread_loop(recv: mpmc::Receiver<WorkItem>, send: mpsc::SyncSender<WorkComplete>) {
for work in recv.into_iter() {
match work {
WorkItem::CreateCSG(
device,
trace_module,
normals_module,
render_pass,
cache,
modules,
debug,
index,
) => {
let csg_start = Instant::now();
let parts = create_csg();
let trace_shader_module =
sdf_specialize_module(device.clone(), &parts, trace_module, &format!("trace"));
let normals_shader_module = sdf_specialize_module(
device.clone(),
&parts,
normals_module,
&format!("normals"),
);
let (deferred_pipeline, normals_pipeline) = deferred_pipelines_recompile(
device,
render_pass,
cache,
modules,
trace_shader_module,
normals_shader_module,
debug,
);
let csg = Arc::new(RwLock::new(CSG {
name: "example".to_string(),
parts,
pos: Vec3::ZERO,
rot: Vec3::ZERO,
scale: Vec3::ONE,
deferred_pipeline,
normals_pipeline,
}));
send.send(WorkComplete::CreateCSG(csg, index)).unwrap();
let csg_end = Instant::now();
info!(
"CSG compile took {} milliseconds",
(csg_end - csg_start).as_secs_f64() * 1000.0
);
},
WorkItem::GetPushConstants(csg, time, index) => {
let mut push_constants = PushConstantData {
world: Mat4::IDENTITY.to_cols_array_2d(),
dimensions: Vec3::ONE.to_array().into(),
lowest_corner: Vec3::ZERO.into(),
enable: [0; 16],
material: 0,
};
let csg = csg.read().unwrap();
push_constants.world = (Mat4::from_translation(csg.pos * 0.01)
* Mat4::from_euler(
EulerRot::XYZ,
csg.rot.x.to_radians(),
csg.rot.y.to_radians(),
csg.rot.z.to_radians(),
)
* Mat4::from_scale(csg.scale * 2.0))
.to_cols_array_2d();
interval_check(&csg, &mut push_constants, time);
push_constants.material = index as _;
send.send(WorkComplete::GetPushConstants(push_constants, index))
.unwrap();
},
}
}
}
fn deferred_pipelines_recompile(
device: Arc<Device>,
render_pass: Arc<RenderPass>,
cache: Arc<PipelineCache>,
shader_modules: ShaderModules,
trace_shader_module: Arc<ShaderModule>,
normals_shader_module: Arc<ShaderModule>,
debug: PreviousDebug,
) -> (Arc<GraphicsPipeline>, Arc<GraphicsPipeline>) {
let specs = get_spec_constants(&debug);
let dynamic_state = [DynamicState::Viewport].into_iter().collect::<HashSet<_>>();
let implicit_vs_entry = shader_modules
.implicit_vs
.specialize(specs.clone())
.unwrap()
.single_entry_point()
.unwrap();
let vertex_input_state = [IVertex::per_vertex()]
.definition(&implicit_vs_entry)
.unwrap();
let implicit_vs_info = PipelineShaderStageCreateInfo::new(implicit_vs_entry);
let implicit_fs_info = PipelineShaderStageCreateInfo::new(
trace_shader_module
.specialize(specs.clone())
.unwrap()
.single_entry_point()
.unwrap(),
);
let layout = PipelineLayout::new(
device.clone(),
PipelineDescriptorSetLayoutCreateInfo::from_stages([&implicit_vs_info, &implicit_fs_info])
.into_pipeline_layout_create_info(device.clone())
.unwrap(),
)
.unwrap();
let stages = [implicit_vs_info.clone(), implicit_fs_info]
.into_iter()
.collect();
let deferred_subpass = Subpass::from(render_pass.clone(), 0).unwrap();
let implicit_pipeline = GraphicsPipeline::new(
device.clone(),
Some(cache.clone()),
GraphicsPipelineCreateInfo {
flags: PipelineCreateFlags::DISABLE_OPTIMIZATION,
stages,
vertex_input_state: Some(vertex_input_state.clone()),
input_assembly_state: Some(InputAssemblyState::default()),
dynamic_state: dynamic_state.clone(),
viewport_state: Some(Default::default()),
rasterization_state: Some(RasterizationState {
front_face: FrontFace::Clockwise,
cull_mode: CullMode::Back,
polygon_mode: PolygonMode::Fill,
..RasterizationState::default()
}),
depth_stencil_state: Some(DepthStencilState {
depth: Some(DepthState::simple()),
..Default::default()
}),
multisample_state: Some(MultisampleState::default()),
color_blend_state: Some(ColorBlendState::with_attachment_states(
deferred_subpass.num_color_attachments(),
ColorBlendAttachmentState::default(),
)),
subpass: Some(deferred_subpass.into()),
..GraphicsPipelineCreateInfo::layout(layout.clone())
},
)
.unwrap();
let fullscreen_vs_entry = shader_modules
.fullscreen_vs
.specialize(specs.clone())
.unwrap()
.single_entry_point()
.unwrap();
let vertex_input_state = VertexInputState::new();
let fullscreen_vs_info = PipelineShaderStageCreateInfo::new(fullscreen_vs_entry);
let normals_fs_info = PipelineShaderStageCreateInfo::new(
normals_shader_module
.specialize(specs.clone())
.unwrap()
.single_entry_point()
.unwrap(),
);
let layout = PipelineLayout::new(
device.clone(),
PipelineDescriptorSetLayoutCreateInfo::from_stages([&fullscreen_vs_info, &normals_fs_info])
.into_pipeline_layout_create_info(device.clone())
.unwrap(),
)
.unwrap();
let stages = [fullscreen_vs_info.clone(), normals_fs_info]
.into_iter()
.collect();
let normals_subpass = Subpass::from(render_pass.clone(), 1).unwrap();
let normals_pipeline = GraphicsPipeline::new(
device.clone(),
Some(cache.clone()),
GraphicsPipelineCreateInfo {
flags: PipelineCreateFlags::DISABLE_OPTIMIZATION,
stages,
vertex_input_state: Some(vertex_input_state.clone()),
input_assembly_state: Some(InputAssemblyState::default()),
dynamic_state: dynamic_state.clone(),
viewport_state: Some(Default::default()),
rasterization_state: Some(RasterizationState {
front_face: FrontFace::Clockwise,
cull_mode: CullMode::Back,
polygon_mode: PolygonMode::Fill,
..RasterizationState::default()
}),
depth_stencil_state: None,
multisample_state: Some(MultisampleState::default()),
color_blend_state: Some(ColorBlendState::with_attachment_states(
normals_subpass.num_color_attachments(),
ColorBlendAttachmentState::default(),
)),
subpass: Some(normals_subpass.into()),
..GraphicsPipelineCreateInfo::layout(layout.clone())
},
)
.unwrap();
(implicit_pipeline, normals_pipeline)
}
fn create_csg() -> SSATape {
let mut tape = SSATape::default();
let pos = tape.push_instruction(
SSAOpcodeSized {
opcode: SSAOpcode::SSAPosition,
size: 1,
},
vec![],
);
let offset = tape.push_instruction(
SSAOpcodeSized {
opcode: SSAOpcode::SSAAdd,
size: 3,
},
vec![
pos[0],
pos[1],
pos[2],
SSAInput::Constant(rand::random_range(0.0..3.0)),
SSAInput::Constant(rand::random_range(0.0..3.0)),
SSAInput::Constant(rand::random_range(0.0..3.0)),
],
);
let sphere = tape.push_instruction(
SSAOpcodeSized {
opcode: SSAOpcode::SSASDFSphere,
size: 1,
},
vec![
offset[0],
offset[1],
offset[2],
SSAInput::Constant(rand::random_range(0.0..1.0)),
],
);
let torus = tape.push_instruction(
SSAOpcodeSized {
opcode: SSAOpcode::SSASDFTorus,
size: 1,
},
vec![
pos[0],
pos[1],
pos[2],
SSAInput::Constant(rand::random_range(0.0..1.0)),
SSAInput::Constant(rand::random_range(0.0..1.0)),
],
);
let min = tape.push_instruction(
SSAOpcodeSized {
opcode: SSAOpcode::SSAMin,
size: 1,
},
vec![sphere[0], torus[0]],
);
tape.push_instruction(
SSAOpcodeSized {
opcode: SSAOpcode::SSAReturn,
size: 1,
},
min,
);
tape
}
fn interval_check(csg: &CSG, push_constants: &mut PushConstantData, time: f32) {
const INTERPRET_INPUT_X: interpreter::Value =
interpreter::Value::from_array([10000.0, 0.0, 0.0, -10000.0, 0.0, 0.0, 0.0, 0.0]);
const INTERPRET_INPUT_Y: interpreter::Value =
interpreter::Value::from_array([0.0, 10000.0, 0.0, 0.0, -10000.0, 0.0, 0.0, 0.0]);
const INTERPRET_INPUT_Z: interpreter::Value =
interpreter::Value::from_array([0.0, 0.0, 10000.0, 0.0, 0.0, -10000.0, 0.0, 0.0]);
const INTERPRET_MUL: interpreter::Value =
interpreter::Value::from_array([-1.0, -1.0, -1.0, 1.0, 1.0, 1.0, 0.0, 0.0]);
const INTERPRET_ADD: interpreter::Value = interpreter::Value::from_array([
10000.0, 10000.0, 10000.0, -10000.0, -10000.0, -10000.0, 0.0, 0.0,
]);
let mut interpreter = PointInterpreter::new(csg);
let interpreter_out = interpreter
.scene(
INTERPRET_INPUT_X,
INTERPRET_INPUT_Y,
INTERPRET_INPUT_Z,
interpreter::Value::splat(time),
)
.mul_add(INTERPRET_MUL, INTERPRET_ADD)
.to_array();
let highest_corner = Vec3::new(interpreter_out[0], interpreter_out[1], interpreter_out[2]);
let lowest_corner = Vec3::new(interpreter_out[3], interpreter_out[4], interpreter_out[5]);
push_constants.dimensions = (highest_corner - lowest_corner).to_array().into();
push_constants.lowest_corner = lowest_corner.into();
push_constants.enable = [0; 16];
for x in 0..8 {
for y in 0..8 {
let interval_input_z: Interval = Interval::new_unchecked(
interpreter::Value::from_array([
(0.0 / 8.0) * push_constants.dimensions[2] + push_constants.lowest_corner[2],
(1.0 / 8.0) * push_constants.dimensions[2] + push_constants.lowest_corner[2],
(2.0 / 8.0) * push_constants.dimensions[2] + push_constants.lowest_corner[2],
(3.0 / 8.0) * push_constants.dimensions[2] + push_constants.lowest_corner[2],
(4.0 / 8.0) * push_constants.dimensions[2] + push_constants.lowest_corner[2],
(5.0 / 8.0) * push_constants.dimensions[2] + push_constants.lowest_corner[2],
(6.0 / 8.0) * push_constants.dimensions[2] + push_constants.lowest_corner[2],
(7.0 / 8.0) * push_constants.dimensions[2] + push_constants.lowest_corner[2],
]),
interpreter::Value::from_array([
(1.0 / 8.0) * push_constants.dimensions[2] + push_constants.lowest_corner[2],
(2.0 / 8.0) * push_constants.dimensions[2] + push_constants.lowest_corner[2],
(3.0 / 8.0) * push_constants.dimensions[2] + push_constants.lowest_corner[2],
(4.0 / 8.0) * push_constants.dimensions[2] + push_constants.lowest_corner[2],
(5.0 / 8.0) * push_constants.dimensions[2] + push_constants.lowest_corner[2],
(6.0 / 8.0) * push_constants.dimensions[2] + push_constants.lowest_corner[2],
(7.0 / 8.0) * push_constants.dimensions[2] + push_constants.lowest_corner[2],
(8.0 / 8.0) * push_constants.dimensions[2] + push_constants.lowest_corner[2],
]),
);
let mut interpreter = IntervalInterpreter::new(csg);
let output = interpreter.scene(
Interval::const_splat2(
(x as f32 / 8.0) * push_constants.dimensions[0]
+ push_constants.lowest_corner[0],
((x + 1) as f32 / 8.0) * push_constants.dimensions[0]
+ push_constants.lowest_corner[0],
),
Interval::const_splat2(
(y as f32 / 8.0) * push_constants.dimensions[1]
+ push_constants.lowest_corner[1],
((y + 1) as f32 / 8.0) * push_constants.dimensions[1]
+ push_constants.lowest_corner[1],
),
interval_input_z,
Interval::const_splat(time),
);
let output = ((output.lower().simd_le(VALUE_0) & output.upper().simd_ge(VALUE_0))
.to_bitmask() as u32)
<< ((y & 0b11) * 8);
push_constants.enable[(x << 1) | (y >> 2)] |= output;
}
}
}
fn sdf_specialize_module(
device: Arc<Device>,
parts: &SSATape,
module: Module,
name: &str,
) -> Arc<ShaderModule> {
let new_module = parts.compile_to_spirv(Some(module));
let assembled_module = new_module.assemble();
let module_as_bytes = assembled_module
.iter()
.cloned()
.map(u32::to_le_bytes)
.flatten()
.collect::<Vec<_>>();
if DUMP_SPV_TO_FILE {
File::create(format!("{name}.out.spv"))
.unwrap()
.write(&module_as_bytes)
.unwrap();
}
unsafe {
::vulkano::shader::ShaderModule::new(device, ShaderModuleCreateInfo::new(&assembled_module))
.unwrap()
}
}