diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 5e7244c6..fcaeb1ec 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -12,7 +12,7 @@ repos: types: [text] files: '\.(cpp|cxx|c|h|hpp|hxx|txx)$' - repo: https://github.com/pre-commit/pre-commit-hooks - rev: v5.0.0 + rev: v6.0.0 hooks: - id: check-added-large-files args: ['--maxkb=1800'] @@ -30,12 +30,12 @@ repos: doc/doxygen-awesome.* )$ - repo: https://github.com/astral-sh/ruff-pre-commit - rev: 'v0.12.7' + rev: 'v0.12.9' hooks: - id: ruff args: [--fix, --exit-non-zero-on-fix] - id: ruff-format - repo: https://github.com/BlankSpruce/gersemi - rev: 0.21.0 + rev: 0.22.1 hooks: - id: gersemi diff --git a/CHANGELOG.md b/CHANGELOG.md index 1bb5859c..64c486c9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,35 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +### Added + +- core/DepthAndShadowPass : add support for multisampled depth/shadow passes +- multibody : add MSAA support in `RobotScene` and `Visualizer` +- core/RenderContext.h : implement move ctor and assignment op +- multibody/Visualizer : working high DPI support +- core/GraphicsPipeline : store pipeline metadata +- core/Texture.h : add comparison operator +- core/Texture.h : add `sampleCount()` getter +- core : add RAII class `GraphicsPipeline` +- multibody : add wireframe mode switch in Visualizer GUI +- multibody/RobotDebug : add external forces to debug elts GUI +- core/DebugScene : add getters for subsystems + +### Changed + +- multibody/RobotScene : always render to G-buffer normal map +- core: move header `LoadCoalGeometries.h` and its functions to library core +- multibody/Visualizer : rename a data member in Config +- core/Device.h : implement move assignment operator +- core/RenderContext : introduce intermediate color render target, start introducing MSAA texture buffers +- core : move contents on `Scene.h` into `Core.h` header +- multibody : better graphics pipeline management using RAII class +- gui : Move all GUI functions into "gui" namespaces + +### Removed + +- Remove deprecated header `candlewick/multibody/LoadCoalPrimitives.h` + ## [0.9.0] - 2025-08-12 - utils/VideoRecorder : check filename extension (must be ".mp4") diff --git a/bindings/python/src/expose-renderer.cpp b/bindings/python/src/expose-renderer.cpp index 91ac306f..5df45119 100644 --- a/bindings/python/src/expose-renderer.cpp +++ b/bindings/python/src/expose-renderer.cpp @@ -11,6 +11,22 @@ void exposeRenderer() { .def("driverName", &Device::driverName, ("self"_a)) .def("shaderFormats", &Device::shaderFormats, ("self"_a)); + bp::class_("Window", bp::no_init) + .def("pixelDensity", &Window::pixelDensity, ("self"_a)) + .def("displayScale", &Window::displayScale, ("self"_a)) + .def( + "title", +[](const Window &w) { return w.title().data(); }, + ("self"_a)); + +#define _c(name) value(#name, SDL_GPUSampleCount::name) + bp::enum_("SampleCount") + ._c(SDL_GPU_SAMPLECOUNT_1) + ._c(SDL_GPU_SAMPLECOUNT_2) + ._c(SDL_GPU_SAMPLECOUNT_4) + ._c(SDL_GPU_SAMPLECOUNT_8) +#undef _c + .export_values(); + bp::def("get_num_gpu_drivers", SDL_GetNumGPUDrivers, "Get number of available GPU drivers."); @@ -36,5 +52,9 @@ void exposeRenderer() { "Automatically detect the compatible set of shader formats."}); bp::class_("RenderContext", bp::no_init) - .def_readonly("device", &RenderContext::device); + .def_readonly("device", &RenderContext::device) + .def_readonly("window", &RenderContext::window) + .add_property("hasDepthTexture", &RenderContext::hasDepthTexture) + .def("enableMSAA", &RenderContext::enableMSAA, ("self"_a, "samples")) + .def("disableMSAA", &RenderContext::disableMSAA, ("self"_a)); } diff --git a/examples/ColoredCube.cpp b/examples/ColoredCube.cpp index 64a5ce1d..51e03fac 100644 --- a/examples/ColoredCube.cpp +++ b/examples/ColoredCube.cpp @@ -99,7 +99,6 @@ int main() { Window{window}, // take ownership of existing SDL_Window handle depth_stencil_format); Device &device = ctx.device; - SDL_GPUTexture *depthTexture = ctx.depth_texture; // Buffers @@ -216,7 +215,6 @@ int main() { SDL_GPURenderPass *render_pass; SDL_GPUBufferBinding vertex_binding = mesh.getVertexBinding(0); CommandBuffer cmdbuf{device}; - SDL_GPUTexture *&swapchain = ctx.swapchain; const Float3 center{0., 0., 0.}; Float3 eye{0., 0., 0.}; // start at phi -> eye.x = 2.5, eye.y = 0.5 @@ -227,10 +225,12 @@ int main() { projViewMat = perp * view * modelMat; if (ctx.waitAndAcquireSwapchain(cmdbuf)) { - SDL_GPUColorTargetInfo ctinfo{.texture = swapchain, - .clear_color = SDL_FColor{}, - .load_op = SDL_GPU_LOADOP_CLEAR, - .store_op = SDL_GPU_STOREOP_STORE}; + SDL_GPUColorTargetInfo ctinfo{ + .texture = ctx.colorTarget(), + .clear_color = SDL_FColor{}, + .load_op = SDL_GPU_LOADOP_CLEAR, + .store_op = SDL_GPU_STOREOP_STORE, + }; SDL_GPUDepthStencilTargetInfo depth_target; SDL_zero(depth_target); depth_target.clear_depth = 1.0f; @@ -238,7 +238,7 @@ int main() { depth_target.store_op = SDL_GPU_STOREOP_DONT_CARE; depth_target.stencil_load_op = SDL_GPU_LOADOP_DONT_CARE; depth_target.stencil_store_op = SDL_GPU_STOREOP_DONT_CARE; - depth_target.texture = depthTexture; + depth_target.texture = ctx.depthTarget(); depth_target.cycle = true; render_pass = SDL_BeginGPURenderPass(cmdbuf, &ctinfo, 1, &depth_target); SDL_BindGPUGraphicsPipeline(render_pass, pipeline); @@ -254,6 +254,7 @@ int main() { SDL_Log("Failed to acquire swapchain: %s", SDL_GetError()); break; } + ctx.presentToSwapchain(cmdbuf); cmdbuf.submit(); } diff --git a/examples/LitMesh.cpp b/examples/LitMesh.cpp index 8a19202a..a3ffade5 100644 --- a/examples/LitMesh.cpp +++ b/examples/LitMesh.cpp @@ -2,6 +2,7 @@ #include "candlewick/core/RenderContext.h" #include "candlewick/core/Mesh.h" +#include "candlewick/core/GraphicsPipeline.h" #include "candlewick/core/Shader.h" #include "candlewick/utils/MeshData.h" #include "candlewick/utils/LoadMesh.h" @@ -78,8 +79,7 @@ int main() { /** CREATE PIPELINE **/ SDL_GPUDepthStencilTargetInfo depth_target_info; - SDL_GPUGraphicsPipeline *pipeline; - { + GraphicsPipeline pipeline = [&]() { auto vertexShader = Shader::fromMetadata(device, "PbrBasic.vert"); auto fragmentShader = Shader::fromMetadata(device, "PbrBasic.frag"); @@ -94,7 +94,7 @@ int main() { depth_target_info.store_op = SDL_GPU_STOREOP_DONT_CARE; depth_target_info.stencil_load_op = SDL_GPU_LOADOP_DONT_CARE; depth_target_info.stencil_store_op = SDL_GPU_STOREOP_DONT_CARE; - depth_target_info.texture = ctx.depth_texture; + depth_target_info.texture = ctx.depthTarget(); depth_target_info.cycle = true; // create pipeline @@ -103,24 +103,27 @@ int main() { .fragment_shader = fragmentShader, .vertex_input_state = meshes[0].layout(), .primitive_type = meshDatas[0].primitiveType, - .rasterizer_state{.fill_mode = SDL_GPU_FILLMODE_FILL, - .cull_mode = SDL_GPU_CULLMODE_NONE, - .front_face = SDL_GPU_FRONTFACE_COUNTER_CLOCKWISE}, - .depth_stencil_state{.compare_op = SDL_GPU_COMPAREOP_LESS_OR_EQUAL, - .enable_depth_test = true, - .enable_depth_write = true}, - .target_info{.color_target_descriptions = &color_target_desc, - .num_color_targets = 1, - .depth_stencil_format = ctx.depthFormat(), - .has_depth_stencil_target = true}, + .rasterizer_state{ + .fill_mode = SDL_GPU_FILLMODE_FILL, + .cull_mode = SDL_GPU_CULLMODE_NONE, + .front_face = SDL_GPU_FRONTFACE_COUNTER_CLOCKWISE, + }, + .multisample_state{}, + .depth_stencil_state{ + .compare_op = SDL_GPU_COMPAREOP_LESS_OR_EQUAL, + .enable_depth_test = true, + .enable_depth_write = true, + }, + .target_info{ + .color_target_descriptions = &color_target_desc, + .num_color_targets = 1, + .depth_stencil_format = ctx.depthFormat(), + .has_depth_stencil_target = true, + }, .props = 0, }; - pipeline = SDL_CreateGPUGraphicsPipeline(device, &pipeline_desc); - } - if (pipeline == NULL) { - SDL_Log("Failed to create pipeline: %s", SDL_GetError()); - return 1; - } + return GraphicsPipeline(device, pipeline_desc, nullptr); + }(); Rad fov = 55.0_degf; CylindricalCamera camera{Camera{ @@ -197,7 +200,7 @@ int main() { } else { SDL_GPUColorTargetInfo ctinfo{ - .texture = ctx.swapchain, + .texture = ctx.colorTarget(), .clear_color = SDL_FColor{0., 0., 0., 0.}, .load_op = SDL_GPU_LOADOP_CLEAR, .store_op = SDL_GPU_STOREOP_STORE, @@ -205,7 +208,7 @@ int main() { }; render_pass = SDL_BeginGPURenderPass(command_buffer, &ctinfo, 1, &depth_target_info); - SDL_BindGPUGraphicsPipeline(render_pass, pipeline); + pipeline.bind(render_pass); TransformUniformData cameraUniform{ .modelView = modelView.matrix(), @@ -231,6 +234,7 @@ int main() { SDL_EndGPURenderPass(render_pass); } + ctx.presentToSwapchain(command_buffer); command_buffer.submit(); frameNo++; } @@ -238,8 +242,7 @@ int main() { for (auto &mesh : meshes) { mesh.release(); } - SDL_ReleaseGPUGraphicsPipeline(device, pipeline); - + pipeline.release(); ctx.destroy(); SDL_Quit(); return 0; diff --git a/examples/MeshNormalsRgb.cpp b/examples/MeshNormalsRgb.cpp index 11642d14..1610ad90 100644 --- a/examples/MeshNormalsRgb.cpp +++ b/examples/MeshNormalsRgb.cpp @@ -2,6 +2,7 @@ #include "candlewick/core/RenderContext.h" #include "candlewick/core/CommandBuffer.h" +#include "candlewick/core/GraphicsPipeline.h" #include "candlewick/core/Mesh.h" #include "candlewick/core/Shader.h" #include "candlewick/utils/MeshData.h" @@ -67,11 +68,9 @@ int main() { Shader vertexShader{device, "VertexNormal.vert", {.uniform_buffers = 1}}; Shader fragmentShader{device, "VertexNormal.frag", {}}; - SDL_GPUTexture *depthTexture = ctx.depth_texture; - SDL_GPUColorTargetDescription colorTarget; SDL_zero(colorTarget); - colorTarget.format = SDL_GetGPUSwapchainTextureFormat(device, window); + colorTarget.format = ctx.colorFormat(); SDL_GPUDepthStencilTargetInfo depthTarget; SDL_zero(depthTarget); depthTarget.clear_depth = 1.0; @@ -79,39 +78,37 @@ int main() { depthTarget.store_op = SDL_GPU_STOREOP_DONT_CARE; depthTarget.stencil_load_op = SDL_GPU_LOADOP_DONT_CARE; depthTarget.stencil_store_op = SDL_GPU_STOREOP_DONT_CARE; - depthTarget.texture = depthTexture; + depthTarget.texture = ctx.depthTarget(); depthTarget.cycle = true; // create pipeline - SDL_GPUGraphicsPipelineCreateInfo pipeline_desc{ - .vertex_shader = vertexShader, - .fragment_shader = fragmentShader, - .vertex_input_state = meshes[0].layout(), - .primitive_type = meshDatas[0].primitiveType, - .rasterizer_state{ - .fill_mode = SDL_GPU_FILLMODE_FILL, - .cull_mode = SDL_GPU_CULLMODE_NONE, - .front_face = SDL_GPU_FRONTFACE_COUNTER_CLOCKWISE, - }, - .depth_stencil_state{ - .compare_op = SDL_GPU_COMPAREOP_LESS_OR_EQUAL, - .enable_depth_test = true, - .enable_depth_write = true, + GraphicsPipeline pipeline{ + device, + { + .vertex_shader = vertexShader, + .fragment_shader = fragmentShader, + .vertex_input_state = meshes[0].layout(), + .primitive_type = meshDatas[0].primitiveType, + .rasterizer_state{ + .fill_mode = SDL_GPU_FILLMODE_FILL, + .cull_mode = SDL_GPU_CULLMODE_NONE, + .front_face = SDL_GPU_FRONTFACE_COUNTER_CLOCKWISE, + }, + .depth_stencil_state{ + .compare_op = SDL_GPU_COMPAREOP_LESS_OR_EQUAL, + .enable_depth_test = true, + .enable_depth_write = true, + }, + .target_info{ + .color_target_descriptions = &colorTarget, + .num_color_targets = 1, + .depth_stencil_format = ctx.depthFormat(), + .has_depth_stencil_target = true, + }, + .props = 0, }, - .target_info{ - .color_target_descriptions = &colorTarget, - .num_color_targets = 1, - .depth_stencil_format = ctx.depthFormat(), - .has_depth_stencil_target = true, - }, - .props = 0, + "Main color pipeline", }; - SDL_GPUGraphicsPipeline *pipeline = - SDL_CreateGPUGraphicsPipeline(device, &pipeline_desc); - if (pipeline == NULL) { - SDL_Log("Failed to create pipeline: %s", SDL_GetError()); - return 1; - } vertexShader.release(); fragmentShader.release(); @@ -191,10 +188,8 @@ int main() { SDL_Log("Failed to acquire swapchain: %s", SDL_GetError()); break; } else { - auto *swapchain = ctx.swapchain; - SDL_GPUColorTargetInfo ctinfo{ - .texture = swapchain, + .texture = ctx.colorTarget(), .clear_color = SDL_FColor{0., 0., 0., 0.}, .load_op = SDL_GPU_LOADOP_CLEAR, .store_op = SDL_GPU_STOREOP_STORE, @@ -202,7 +197,7 @@ int main() { }; render_pass = SDL_BeginGPURenderPass(command_buffer, &ctinfo, 1, &depthTarget); - SDL_BindGPUGraphicsPipeline(render_pass, pipeline); + pipeline.bind(render_pass); TransformUniformData cameraUniform{ projViewMat, normalMatrix, @@ -224,8 +219,7 @@ int main() { for (auto &mesh : meshes) { mesh.release(); } - SDL_ReleaseGPUGraphicsPipeline(device, pipeline); - + pipeline.release(); ctx.destroy(); SDL_Quit(); return 0; diff --git a/examples/Ur5WithSystems.cpp b/examples/Ur5WithSystems.cpp index 0c595f4b..778e8c6d 100644 --- a/examples/Ur5WithSystems.cpp +++ b/examples/Ur5WithSystems.cpp @@ -49,14 +49,16 @@ namespace pin = pinocchio; using namespace candlewick; +using multibody::RobotDebugSystem; using multibody::RobotScene; using multibody::RobotSpec; /// Application constants -constexpr Uint32 wWidth = 1920; -constexpr Uint32 wHeight = 1050; -constexpr float aspectRatio = float(wWidth) / float(wHeight); +static std::array wDims{1600u, 960u}; +const auto &[wWidth, wHeight] = wDims; + +static float aspectRatio; /// Application state @@ -64,10 +66,7 @@ static Radf currentFov = 55.0_degf; static float nearZ = 0.01f; static float farZ = 10.f; static float currentOrthoScale = 1.f; -static CylindricalCamera g_camera{{ - .projection = perspectiveFromFov(currentFov, aspectRatio, nearZ, farZ), - .view = Eigen::Isometry3f{lookAt({2.0, 0, 2.}, Float3::Zero())}, -}}; +static CylindricalCamera g_camera; static CameraProjection g_cameraType = CameraProjection::PERSPECTIVE; static bool quitRequested = false; static bool showFrustum = false; @@ -78,9 +77,6 @@ enum VizMode { }; static VizMode g_showDebugViz = FULL_RENDER; -static float pixelDensity; -static float displayScale; - static void updateFov(Radf newFov) { g_camera.camera.projection = perspectiveFromFov(newFov, aspectRatio, nearZ, farZ); @@ -114,8 +110,7 @@ loadGeomObjFromFile(const char *name, std::string_view filename, void eventLoop(const RenderContext &renderer) { // update pixel density and display scale - pixelDensity = renderer.window.pixelDensity(); - displayScale = renderer.window.displayScale(); + float pixelDensity = renderer.window.pixelDensity(); const float rotSensitivity = 5e-3f * pixelDensity; const float panSensitivity = 1e-2f * pixelDensity; SDL_Event event; @@ -205,17 +200,16 @@ static void addTeapotGeometry(pin::GeometryModel &geom_model) { geom_model.addGeometryObject(convex_obj); } -static void screenshot_button_callback(RenderContext &renderer, - media::TransferBufferPool &pool, - const char *filename) { +static void screenshotButtonCallback(const RenderContext &renderer, + media::TransferBufferPool &pool, + const char *filename) { const auto &device = renderer.device; CommandBuffer command_buffer{device}; - renderer.waitAndAcquireSwapchain(command_buffer); SDL_Log("Saving screenshot at %s", filename); - media::saveTextureToFile(command_buffer, device, pool, renderer.swapchain, - renderer.getSwapchainTextureFormat(), wWidth, - wHeight, filename); + media::saveTextureToFile(command_buffer, device, pool, + renderer.resolvedColorTarget(), + renderer.colorFormat(), wWidth, wHeight, filename); } static const RobotSpec ur_robot_spec = @@ -227,15 +221,23 @@ static const RobotSpec ur_robot_spec = } .ensure_absolute_filepaths(); +static void initialize() { + aspectRatio = float(wWidth) / float(wHeight); + g_camera = CylindricalCamera{{ + .projection = perspectiveFromFov(currentFov, aspectRatio, nearZ, farZ), + .view = Eigen::Isometry3f{lookAt({2.0, 0, 2.}, Float3::Zero())}, + }}; +} + int main(int argc, char **argv) { CLI::App app{"Ur5 example"}; bool performRecording{false}; RobotScene::Config robot_scene_config; robot_scene_config.triangle_has_prepass = true; - robot_scene_config.enable_normal_target = true; argv = app.ensure_utf8(argv); app.add_flag("-r,--record", performRecording, "Record output"); + app.add_option("--dims", wDims, "Window dimensions.")->capture_default_str(); CLI11_PARSE(app, argc, argv); if (!SDL_Init(SDL_INIT_VIDEO)) @@ -244,9 +246,11 @@ int main(int argc, char **argv) { // D16_UNORM works on macOS, D24_UNORM and D32_FLOAT break the depth prepass RenderContext renderer{ Device{auto_detect_shader_format_subset(), false}, - Window(__FILE__, wWidth, wHeight, 0), + Window(__FILE__, int(wWidth), int(wHeight), + SDL_WINDOW_HIGH_PIXEL_DENSITY), SDL_GPU_TEXTUREFORMAT_D16_UNORM, }; + initialize(); entt::registry registry{}; @@ -319,24 +323,25 @@ int main(int argc, char **argv) { // DEBUG SYSTEM + using namespace entt::literals; DebugScene debug_scene{registry, renderer}; auto &robot_debug = - debug_scene.addSystem(model, pin_data); - auto [triad_id, triad] = debug_scene.addTriad(); + debug_scene.addSystem("robot"_hs, model, pin_data); auto [grid_id, grid] = debug_scene.addLineGrid(0xE0A236ff_rgbaf); pin::FrameIndex ee_frame_id = model.getFrameId("ee_link"); + robot_debug.addFrameTriad(0ul); robot_debug.addFrameTriad(ee_frame_id); robot_debug.addFrameVelocityArrow(ee_frame_id); DepthPass depthPass(renderer.device, plane_obj.mesh.layout(), - renderer.depth_texture, + renderer.depthTarget(), {SDL_GPU_CULLMODE_NONE, 0.05f, 0.f, true, false}); auto &shadowPassInfo = robot_scene.shadowPass; auto shadowDebugPass = DepthDebugPass::create(renderer, shadowPassInfo.shadowMap); auto depthDebugPass = - DepthDebugPass::create(renderer, renderer.depth_texture); + DepthDebugPass::create(renderer, renderer.depthTarget()); DepthDebugPass::VizStyle depth_mode = DepthDebugPass::VIZ_GRAYSCALE; FrustumBoundsDebugSystem frustumBoundsDebug{registry, renderer}; @@ -353,7 +358,7 @@ int main(int argc, char **argv) { static bool show_plane_vis = true; if (show_about_window) - showCandlewickAboutWindow(&show_about_window); + gui::showCandlewickAboutWindow(&show_about_window); if (show_imgui_window) ImGui::ShowAboutWindow(&show_imgui_window); @@ -372,7 +377,7 @@ int main(int argc, char **argv) { ImGui::Text("Video driver: %s", SDL_GetCurrentVideoDriver()); ImGui::SameLine(); ImGui::Text("Device driver: %s", r.device.driverName()); - ImGui::Text("Display pixel density: %.2f / scale: %.2f", + ImGui::Text("Window pixel density: %.2f / display scale: %.2f", r.window.pixelDensity(), r.window.displayScale()); ImGui::SeparatorText("Camera"); bool ortho_change, persp_change; @@ -403,10 +408,9 @@ int main(int argc, char **argv) { } ImGui::SeparatorText("Env. status"); - guiAddDisableCheckbox("Render plane", registry, plane_entity, - show_plane_vis); + gui::addDisableCheckbox("Render plane", registry, plane_entity, + show_plane_vis); ImGui::Checkbox("Render grid", &grid.enable); - ImGui::Checkbox("Render triad", &triad.enable); ImGui::Checkbox("Render frustum", &showFrustum); ImGui::Checkbox("Ambient occlusion (SSAO)", @@ -427,7 +431,8 @@ int main(int argc, char **argv) { ImGui::SeparatorText("Screenshots"); static std::string scr_filename; - guiAddFileDialog(renderer.window, DialogFileType::IMAGES, scr_filename); + gui::addFileDialog(renderer.window, DialogFileType::IMAGES, + scr_filename); if (ImGui::Button("Take screenshot")) { if (scr_filename.empty()) generateMediaFilenameFromTimestamp("cdw_screenshot", scr_filename); @@ -436,11 +441,11 @@ int main(int argc, char **argv) { ImGui::SeparatorText("Robot model"); ImGui::SetItemTooltip("Information about the displayed robot model."); - multibody::guiAddPinocchioModelInfo(registry, model, geom_model); + multibody::gui::addPinocchioModelInfo(registry, model, geom_model); ImGui::SeparatorText("Lights"); - guiAddLightControls(robot_scene.directionalLight, - robot_scene.numLights()); + gui::addLightControls(robot_scene.directionalLight, + robot_scene.numLights()); ImGui::Separator(); ImGui::ColorEdit4("grid color", grid.colors[0].data(), @@ -495,8 +500,8 @@ int main(int argc, char **argv) { pin::updateFramePlacements(model, pin_data); pin::updateGeometryPlacements(model, pin_data, geom_model, geom_data); q = qn; + robot_scene.update(); debug_scene.update(); - robot_scene.updateTransforms(); // acquire command buffer and swapchain CommandBuffer command_buffer = renderer.acquireCommandBuffer(); @@ -538,22 +543,23 @@ int main(int argc, char **argv) { continue; } + renderer.presentToSwapchain(command_buffer); command_buffer.submit(); if (performRecording) { #ifdef CANDLEWICK_WITH_FFMPEG_SUPPORT CommandBuffer command_buffer = renderer.acquireCommandBuffer(); - auto swapchain_format = renderer.getSwapchainTextureFormat(); - recorder.writeTextureToVideoFrame(command_buffer, renderer.device, - transfer_buffer_pool, - renderer.swapchain, swapchain_format); + recorder.writeTextureToFrame(command_buffer, renderer.device, + transfer_buffer_pool, + renderer.resolvedColorTarget()); #endif } if (screenshot_filename) { - screenshot_button_callback(renderer, transfer_buffer_pool, - screenshot_filename); + screenshotButtonCallback(renderer, transfer_buffer_pool, + screenshot_filename); screenshot_filename = nullptr; } + frameNo++; } diff --git a/examples/Visualizer.cpp b/examples/Visualizer.cpp index 74035cf8..c4ecf6b0 100644 --- a/examples/Visualizer.cpp +++ b/examples/Visualizer.cpp @@ -10,6 +10,7 @@ #include using namespace candlewick::multibody; +using candlewick::sdlSampleToValue; using pinocchio::visualizers::Vector3; using std::chrono::steady_clock; namespace fs = std::filesystem; @@ -23,24 +24,61 @@ static const RobotSpec ur_robot_spec = } .ensure_absolute_filepaths(); +static void addFloor(pin::GeometryModel &geom_model) { + auto coll = std::make_shared(0., 0., 1., -0.1); + pin::GeometryObject object{"plane", 0ul, coll, pin::SE3::Identity()}; + object.meshColor << 1.0, 1.0, 1.0, 1.0; + geom_model.addGeometryObject(object); +} + +static void addBall(pin::GeometryModel &geom_model) { + auto sp = std::make_shared(0.2); + pin::SE3 M = pin::SE3::Identity(); + M.translation() << 0.4, 0.1, 0.3; + pin::GeometryObject object{"sphere", 0ul, sp, M}; + object.meshColor << 1.0, 1.0, 0.2, 0.3; + geom_model.addGeometryObject(object); +} + int main(int argc, char **argv) { CLI::App app{"Visualizer example"}; argv = app.ensure_utf8(argv); std::array window_dims{1920u, 1080u}; double fps; + SDL_GPUSampleCount sample_count{SDL_GPU_SAMPLECOUNT_1}; + const std::map sample_count_map{ + {"1", SDL_GPU_SAMPLECOUNT_1}, + {"2", SDL_GPU_SAMPLECOUNT_2}, + {"4", SDL_GPU_SAMPLECOUNT_4}, + {"8", SDL_GPU_SAMPLECOUNT_8}}; + const auto transform_validator = + CLI::IsMember(sample_count_map) & CLI::Transformer(sample_count_map); app.add_option("--dims", window_dims, "Window dimensions.") ->capture_default_str(); app.add_option("--fps", fps, "Framerate") ->default_val(60); + app.add_option("--msaa", sample_count, "Level of multisample anti-aliasing.") + ->default_function([&sample_count] { + return std::to_string(sdlSampleToValue(sample_count)); + }) + ->transform(transform_validator) + ->capture_default_str(); CLI11_PARSE(app, argc, argv); pin::Model model; pin::GeometryModel geom_model; loadModels(ur_robot_spec, model, &geom_model, NULL); - - Visualizer visualizer{{window_dims[0], window_dims[1]}, model, geom_model}; + addFloor(geom_model); + addBall(geom_model); + + Visualizer::Config config{ + window_dims[0], + window_dims[1], + sample_count, + }; + Visualizer visualizer{config, model, geom_model}; assert(!visualizer.hasExternalData()); visualizer.addFrameViz(model.getFrameId("world"), false, Vector3::Ones()); visualizer.addFrameViz(model.getFrameId("elbow_joint")); diff --git a/shaders/compiled/WBOITComposite.frag.json b/shaders/compiled/WBOITComposite.frag.json index 5fd69af8..bb26206c 100644 --- a/shaders/compiled/WBOITComposite.frag.json +++ b/shaders/compiled/WBOITComposite.frag.json @@ -1 +1 @@ -{ "samplers": 2, "storage_textures": 0, "storage_buffers": 0, "uniform_buffers": 0 } +{ "samplers": 2, "storage_textures": 0, "storage_buffers": 0, "uniform_buffers": 0, "inputs": [], "outputs": [{ "name": "outColor", "type": "float4", "location": 0 }] } diff --git a/shaders/compiled/WBOITComposite.frag.msl b/shaders/compiled/WBOITComposite.frag.msl index ad9e84fb..acf497d6 100644 --- a/shaders/compiled/WBOITComposite.frag.msl +++ b/shaders/compiled/WBOITComposite.frag.msl @@ -8,13 +8,13 @@ struct main0_out float4 outColor [[color(0)]]; }; -fragment main0_out main0(texture2d accumTexture [[texture(0)]], texture2d revealTexture [[texture(1)]], sampler accumTextureSmplr [[sampler(0)]], sampler revealTextureSmplr [[sampler(1)]], float4 gl_FragCoord [[position]]) +fragment main0_out main0(texture2d_ms accumTexture [[texture(0)]], texture2d_ms revealTexture [[texture(1)]], sampler accumTextureSmplr [[sampler(0)]], sampler revealTextureSmplr [[sampler(1)]], float4 gl_FragCoord [[position]], uint gl_SampleID [[sample_id]]) { main0_out out = {}; - float2 viewportSize = float2(int2(accumTexture.get_width(), accumTexture.get_height())); - float2 uv = gl_FragCoord.xy / viewportSize; - float4 accum = accumTexture.sample(accumTextureSmplr, uv); - float reveal = revealTexture.sample(revealTextureSmplr, uv).x; + gl_FragCoord.xy += get_sample_position(gl_SampleID) - 0.5; + int2 uv = int2(gl_FragCoord.xy); + float4 accum = accumTexture.read(uint2(uv), gl_SampleID); + float reveal = revealTexture.read(uint2(uv), gl_SampleID).x; float3 color = accum.xyz; if (accum.w > 0.001000000047497451305389404296875) { diff --git a/shaders/compiled/WBOITComposite.frag.spv b/shaders/compiled/WBOITComposite.frag.spv index 31b75313..f679bb56 100644 Binary files a/shaders/compiled/WBOITComposite.frag.spv and b/shaders/compiled/WBOITComposite.frag.spv differ diff --git a/shaders/src/WBOITComposite.frag b/shaders/src/WBOITComposite.frag index f82726d8..c841e5e1 100644 --- a/shaders/src/WBOITComposite.frag +++ b/shaders/src/WBOITComposite.frag @@ -1,15 +1,14 @@ #version 450 -layout(set=2, binding=0) uniform sampler2D accumTexture; -layout(set=2, binding=1) uniform sampler2D revealTexture; +layout(set=2, binding=0) uniform sampler2DMS accumTexture; +layout(set=2, binding=1) uniform sampler2DMS revealTexture; layout(location = 0) out vec4 outColor; void main() { - vec2 viewportSize = textureSize(accumTexture, 0).xy; - vec2 uv = gl_FragCoord.xy / viewportSize; + ivec2 uv = ivec2(gl_FragCoord.xy); - vec4 accum = texture(accumTexture, uv); - float reveal = texture(revealTexture, uv).r; + vec4 accum = texelFetch(accumTexture, uv, gl_SampleID); + float reveal = texelFetch(revealTexture, uv, gl_SampleID).r; vec3 color = accum.rgb; if (accum.a > 0.001) { diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 7448d9e0..064bf289 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -1,4 +1,4 @@ -# Copyright (c) 2024-2025 ManifoldFR +# Copyright (c) 2024-2025 Inria add_library( candlewick_core @@ -9,10 +9,11 @@ add_library( candlewick/core/DebugScene.cpp candlewick/core/DepthAndShadowPass.cpp candlewick/core/Device.cpp - candlewick/core/math_util.cpp candlewick/core/errors.cpp candlewick/core/file_dialog_gui.cpp candlewick/core/GuiSystem.cpp + candlewick/core/LoadCoalGeometries.cpp + candlewick/core/math_util.cpp candlewick/core/Mesh.cpp candlewick/core/RenderContext.cpp candlewick/core/Shader.cpp diff --git a/src/candlewick/core/CommandBuffer.cpp b/src/candlewick/core/CommandBuffer.cpp index bc6948f1..a121844f 100644 --- a/src/candlewick/core/CommandBuffer.cpp +++ b/src/candlewick/core/CommandBuffer.cpp @@ -4,26 +4,12 @@ namespace candlewick { CommandBuffer::CommandBuffer(const Device &device) { - _cmdBuf = SDL_AcquireGPUCommandBuffer(device); -} - -CommandBuffer::CommandBuffer(CommandBuffer &&other) noexcept - : _cmdBuf(other._cmdBuf) { - other._cmdBuf = nullptr; -} - -CommandBuffer &CommandBuffer::operator=(CommandBuffer &&other) noexcept { - if (active()) { - this->cancel(); - } - _cmdBuf = other._cmdBuf; - other._cmdBuf = nullptr; - return *this; + m_handle = SDL_AcquireGPUCommandBuffer(device); } bool CommandBuffer::cancel() noexcept { - bool ret = SDL_CancelGPUCommandBuffer(_cmdBuf); - _cmdBuf = nullptr; + bool ret = SDL_CancelGPUCommandBuffer(m_handle); + m_handle = nullptr; if (!ret) { SDL_LogWarn(SDL_LOG_CATEGORY_APPLICATION, "Failed to cancel command buffer: %s", SDL_GetError()); diff --git a/src/candlewick/core/CommandBuffer.h b/src/candlewick/core/CommandBuffer.h index 2b983139..13c8b1aa 100644 --- a/src/candlewick/core/CommandBuffer.h +++ b/src/candlewick/core/CommandBuffer.h @@ -15,40 +15,46 @@ concept GpuCompatibleData = (alignof(T) == 4 || alignof(T) == 8 || alignof(T) == 16); class CommandBuffer { - SDL_GPUCommandBuffer *_cmdBuf; + SDL_GPUCommandBuffer *m_handle; public: CommandBuffer(const Device &device); /// \brief Convert to SDL_GPU command buffer handle. - operator SDL_GPUCommandBuffer *() const { return _cmdBuf; } + operator SDL_GPUCommandBuffer *() const { return m_handle; } /// \brief Deleted copy constructor. CommandBuffer(const CommandBuffer &) = delete; /// \brief Move constructor. - CommandBuffer(CommandBuffer &&other) noexcept; + CommandBuffer(CommandBuffer &&other) noexcept : m_handle(other.m_handle) { + other.m_handle = nullptr; + } /// \brief Deleted copy assignment operator. CommandBuffer &operator=(const CommandBuffer &) = delete; /// \brief Move assignment operator. - CommandBuffer &operator=(CommandBuffer &&other) noexcept; - - friend void swap(CommandBuffer &lhs, CommandBuffer &rhs) noexcept { - std::swap(lhs._cmdBuf, rhs._cmdBuf); + CommandBuffer &operator=(CommandBuffer &&other) noexcept { + if (this != &other) { + if (active()) + this->cancel(); + m_handle = other.m_handle; + other.m_handle = nullptr; + } + return *this; } bool submit() noexcept { - if (!(active() && SDL_SubmitGPUCommandBuffer(_cmdBuf))) + if (!(active() && SDL_SubmitGPUCommandBuffer(m_handle))) return false; - _cmdBuf = nullptr; + m_handle = nullptr; return true; } SDL_GPUFence *submitAndAcquireFence() noexcept { - SDL_GPUFence *fence = SDL_SubmitGPUCommandBufferAndAcquireFence(_cmdBuf); - _cmdBuf = nullptr; + SDL_GPUFence *fence = SDL_SubmitGPUCommandBufferAndAcquireFence(m_handle); + m_handle = nullptr; return fence; } @@ -59,15 +65,15 @@ class CommandBuffer { /// \brief Check if the command buffer is still active. /// /// For this wrapper class, it means the internal pointer is non-null. - bool active() const noexcept { return _cmdBuf; } + bool active() const noexcept { return m_handle; } ~CommandBuffer() noexcept { if (active()) { SDL_LogWarn(SDL_LOG_CATEGORY_APPLICATION, - "CommandBuffer object is being destroyed while still active! " + "CommandBuffer object is being destroyed while still active. " "It will be cancelled."); [[maybe_unused]] bool ret = cancel(); - assert(ret); + CANDLEWICK_ASSERT(ret, "Failed to cancel command buffer on cleanup."); } } @@ -105,13 +111,13 @@ class CommandBuffer { /// \brief Push uniform data to the vertex shader. CommandBuffer &pushVertexUniformRaw(Uint32 slot_index, const void *data, Uint32 length) { - SDL_PushGPUVertexUniformData(_cmdBuf, slot_index, data, length); + SDL_PushGPUVertexUniformData(m_handle, slot_index, data, length); return *this; } /// \brief Push uniform data to the fragment shader. CommandBuffer &pushFragmentUniformRaw(Uint32 slot_index, const void *data, Uint32 length) { - SDL_PushGPUFragmentUniformData(_cmdBuf, slot_index, data, length); + SDL_PushGPUFragmentUniformData(m_handle, slot_index, data, length); return *this; } }; diff --git a/src/candlewick/core/Components.h b/src/candlewick/core/Components.h index 1535bd02..8254bcb9 100644 --- a/src/candlewick/core/Components.h +++ b/src/candlewick/core/Components.h @@ -21,9 +21,12 @@ struct TransformComponent : Mat4f { using Mat4f::operator=; }; +enum class RenderMode { FILL, LINE }; + struct MeshMaterialComponent { Mesh mesh; std::vector materials; + RenderMode mode = RenderMode::FILL; MeshMaterialComponent(Mesh &&mesh, std::vector &&materials) : mesh(std::move(mesh)), materials(std::move(materials)) { assert(mesh.numViews() == materials.size()); diff --git a/src/candlewick/core/Core.h b/src/candlewick/core/Core.h index 239a982f..1c6ac247 100644 --- a/src/candlewick/core/Core.h +++ b/src/candlewick/core/Core.h @@ -2,6 +2,7 @@ #include #include +#include #define CANDLEWICK_ASSERT(condition, msg) assert(((condition) && (msg))) @@ -9,6 +10,7 @@ namespace candlewick { struct Camera; class CommandBuffer; struct Device; +class GraphicsPipeline; class Texture; class Mesh; class MeshView; @@ -17,6 +19,18 @@ struct Shader; struct RenderContext; struct Window; +struct DirectionalLight; + using coal::AABB; +/// \brief The Scene concept requires that there exist functions to render the +/// scene. Provided a command buffer and Camera, and that there is a function to +/// release the resources of the Scene. +template +concept Scene = requires(T t, CommandBuffer &cmdBuf, const Camera &camera) { + { t.update() } -> std::same_as; + { t.render(cmdBuf, camera) } -> std::same_as; + { t.release() } -> std::same_as; +}; + } // namespace candlewick diff --git a/src/candlewick/core/DebugScene.cpp b/src/candlewick/core/DebugScene.cpp index 1a26cba7..1a954de9 100644 --- a/src/candlewick/core/DebugScene.cpp +++ b/src/candlewick/core/DebugScene.cpp @@ -6,13 +6,16 @@ #include "../primitives/Arrow.h" #include "../primitives/Grid.h" +#include +#include + namespace candlewick { DebugScene::DebugScene(entt::registry ®, const RenderContext &renderer) : m_registry(reg) , m_renderer(renderer) - , m_trianglePipeline(nullptr) - , m_linePipeline(nullptr) + , m_trianglePipeline(NoInit) + , m_linePipeline(NoInit) , m_sharedMeshes() { this->initializeSharedMeshes(); } @@ -20,13 +23,10 @@ DebugScene::DebugScene(entt::registry ®, const RenderContext &renderer) DebugScene::DebugScene(DebugScene &&other) : m_registry(other.m_registry) , m_renderer(other.m_renderer) - , m_trianglePipeline(other.m_trianglePipeline) - , m_linePipeline(other.m_linePipeline) + , m_trianglePipeline(std::move(other.m_trianglePipeline)) + , m_linePipeline(std::move(other.m_linePipeline)) , m_subsystems(std::move(other.m_subsystems)) - , m_sharedMeshes(std::move(other.m_sharedMeshes)) { - other.m_trianglePipeline = nullptr; - other.m_linePipeline = nullptr; -} + , m_sharedMeshes(std::move(other.m_sharedMeshes)) {} void DebugScene::initializeSharedMeshes() { { @@ -35,14 +35,9 @@ void DebugScene::initializeSharedMeshes() { setupPipelines(mesh.layout()); m_sharedMeshes.emplace(TRIAD, std::move(mesh)); } - { - MeshData grid_data = loadGrid(20); - m_sharedMeshes.emplace(GRID, createMesh(device(), grid_data, true)); - } - { - MeshData arrow_data = loadArrowSolid(false); - m_sharedMeshes.emplace(ARROW, createMesh(device(), arrow_data, true)); - } + m_sharedMeshes.emplace(GRID, createMesh(device(), loadGrid(20), true)); + m_sharedMeshes.emplace(ARROW, + createMesh(device(), loadArrowSolid(false), true)); } std::tuple @@ -65,24 +60,25 @@ DebugScene::addLineGrid(const Float4 &color) { return {entity, item}; } -entt::entity DebugScene::addArrow(const Float4 &color) { +std::tuple +DebugScene::addArrow(const Float4 &color) { auto entity = m_registry.create(); auto &dmc = m_registry.emplace( entity, DebugPipelines::TRIANGLE_FILL, DebugMeshType::ARROW, std::vector{color}, true); dmc.scale << 0.333f, 0.333f, 1.0f; m_registry.emplace(entity, Mat4f::Identity()); - return entity; + return {entity, dmc}; } void DebugScene::setupPipelines(const MeshLayout &layout) { - if (m_linePipeline && m_trianglePipeline) + if (m_linePipeline.initialized() && m_trianglePipeline.initialized()) return; auto vertexShader = Shader::fromMetadata(device(), "Hud3dElement.vert"); auto fragmentShader = Shader::fromMetadata(device(), "Hud3dElement.frag"); SDL_GPUColorTargetDescription color_desc; SDL_zero(color_desc); - color_desc.format = m_renderer.getSwapchainTextureFormat(); + color_desc.format = m_renderer.colorFormat(); SDL_GPUGraphicsPipelineCreateInfo info{ .vertex_shader = vertexShader, .fragment_shader = fragmentShader, @@ -94,6 +90,7 @@ void DebugScene::setupPipelines(const MeshLayout &layout) { .depth_bias_slope_factor = 0.001f, .enable_depth_bias = true, .enable_depth_clip = true}, + .multisample_state{m_renderer.getMsaaSampleCount()}, .depth_stencil_state{.compare_op = SDL_GPU_COMPAREOP_LESS_OR_EQUAL, .enable_depth_test = true, .enable_depth_write = true}, @@ -103,29 +100,35 @@ void DebugScene::setupPipelines(const MeshLayout &layout) { .has_depth_stencil_target = true}, .props = 0, }; - if (!m_trianglePipeline) - m_trianglePipeline = SDL_CreateGPUGraphicsPipeline(device(), &info); + if (!m_trianglePipeline.initialized()) + m_trianglePipeline = GraphicsPipeline(device(), info, "Debug [triangle]"); // re-use info.primitive_type = SDL_GPU_PRIMITIVETYPE_LINELIST; - if (!m_linePipeline) - m_linePipeline = SDL_CreateGPUGraphicsPipeline(device(), &info); + if (!m_linePipeline.initialized()) + m_linePipeline = GraphicsPipeline(device(), info, "Debug [line]"); } void DebugScene::render(CommandBuffer &cmdBuf, const Camera &camera) const { SDL_GPUColorTargetInfo color_target_info; SDL_zero(color_target_info); - color_target_info.texture = m_renderer.swapchain; + color_target_info.texture = m_renderer.colorTarget(); color_target_info.load_op = SDL_GPU_LOADOP_LOAD; color_target_info.store_op = SDL_GPU_STOREOP_STORE; + color_target_info.cycle = false; + // do resolve to the target that's presented to swapchain + // if (m_renderer.msaaEnabled()) { + // color_target_info.resolve_texture = m_renderer.resolvedColorTarget(); + // color_target_info.store_op = SDL_GPU_STOREOP_RESOLVE_AND_STORE; + // } SDL_GPUDepthStencilTargetInfo depth_target_info; SDL_zero(depth_target_info); depth_target_info.load_op = SDL_GPU_LOADOP_LOAD; depth_target_info.store_op = SDL_GPU_STOREOP_STORE; depth_target_info.stencil_load_op = SDL_GPU_LOADOP_LOAD; depth_target_info.stencil_store_op = SDL_GPU_STOREOP_DONT_CARE; - depth_target_info.texture = m_renderer.depth_texture; + depth_target_info.texture = m_renderer.depthTarget(); depth_target_info.cycle = false; SDL_GPURenderPass *render_pass = @@ -142,10 +145,10 @@ void DebugScene::render(CommandBuffer &cmdBuf, const Camera &camera) const { switch (cmd.pipeline_type) { case DebugPipelines::TRIANGLE_FILL: - SDL_BindGPUGraphicsPipeline(render_pass, m_trianglePipeline); + m_trianglePipeline.bind(render_pass); break; case DebugPipelines::TRIANGLE_LINE: - SDL_BindGPUGraphicsPipeline(render_pass, m_linePipeline); + m_linePipeline.bind(render_pass); break; } @@ -164,17 +167,38 @@ void DebugScene::render(CommandBuffer &cmdBuf, const Camera &camera) const { } void DebugScene::release() { - if (device() && m_trianglePipeline) { - SDL_ReleaseGPUGraphicsPipeline(device(), m_trianglePipeline); - m_trianglePipeline = nullptr; - } - if (device() && m_linePipeline) { - SDL_ReleaseGPUGraphicsPipeline(device(), m_linePipeline); - m_linePipeline = nullptr; - } + m_trianglePipeline.release(); + m_linePipeline.release(); + // clean up all DebugMeshComponent objects. auto view = m_registry.view(); m_registry.destroy(view.begin(), view.end()); m_sharedMeshes.clear(); } + +namespace gui { + void addDebugMesh(DebugMeshComponent &dmc, bool enable_pipeline_switch) { + ImGui::Checkbox("##enabled", &dmc.enable); + Uint32 col_id = 0; + ImGuiColorEditFlags color_flags = ImGuiColorEditFlags_NoAlpha | + ImGuiColorEditFlags_NoSidePreview | + ImGuiColorEditFlags_NoInputs; + char label[32]; + for (auto &col : dmc.colors) { + SDL_snprintf(label, sizeof(label), "##color##%u", col_id); + ImGui::SameLine(); + ImGui::ColorEdit4(label, col.data(), color_flags); + col_id++; + } + if (enable_pipeline_switch) { + const char *names[] = {"FILL", "LINE"}; + static_assert(IM_ARRAYSIZE(names) == + magic_enum::enum_count()); + ImGui::SameLine(); + ImGui::Combo("Mode##pipeline", (int *)&dmc.pipeline_type, names, + IM_ARRAYSIZE(names)); + } + } +} // namespace gui + } // namespace candlewick diff --git a/src/candlewick/core/DebugScene.h b/src/candlewick/core/DebugScene.h index e713224e..9111d2d1 100644 --- a/src/candlewick/core/DebugScene.h +++ b/src/candlewick/core/DebugScene.h @@ -1,6 +1,6 @@ #pragma once -#include "Scene.h" +#include "GraphicsPipeline.h" #include "Mesh.h" #include "RenderContext.h" #include "math_types.h" @@ -50,9 +50,11 @@ struct DebugMeshComponent; class DebugScene { entt::registry &m_registry; const RenderContext &m_renderer; - SDL_GPUGraphicsPipeline *m_trianglePipeline{nullptr}; - SDL_GPUGraphicsPipeline *m_linePipeline{nullptr}; - std::vector> m_subsystems; + GraphicsPipeline m_trianglePipeline; + GraphicsPipeline m_linePipeline; + entt::dense_map> + m_subsystems; std::unordered_map m_sharedMeshes; inline static const std::array m_triadColors = { Float4{1., 0., 0., 1.}, @@ -84,11 +86,28 @@ class DebugScene { const entt::registry ®istry() const { return m_registry; } /// \brief Add a subsystem (IDebugSubSystem) to the scene. - template System, typename... Args> - System &addSystem(Args &&...args) { - auto sys = std::make_unique(*this, std::forward(args)...); - auto &p = m_subsystems.emplace_back(std::move(sys)); - return static_cast(*p); + template T, typename... Args> + T &addSystem(entt::hashed_string::hash_type name, Args &&...args) { + auto sys = std::make_unique(*this, std::forward(args)...); + auto [it, flag] = m_subsystems.emplace(name, std::move(sys)); + return static_cast(*it->second); + } + + /// \brief Get a subsystem by key (a hashed string) + /// \tparam T The concrete derived type. + template T> + T *getSystem(entt::hashed_string::hash_type name) { + if (auto it = m_subsystems.find(name); it != m_subsystems.cend()) { + return dynamic_cast(it->second.get()); + } + return nullptr; + } + + /// \sa getSystem(). + template T> + T &tryGetSystem(entt::hashed_string::hash_type name) { + auto *p = m_subsystems.at(name).get(); + return dynamic_cast(*p); } /// \brief Just the basic 3D triad. @@ -98,13 +117,14 @@ class DebugScene { /// \brief Add a basic line grid. /// \param color Grid color. std::tuple - addLineGrid(const Float4 &color = Float4::Ones()); + addLineGrid(const Float4 &color = 0xE0A236FF_rgbaf); /// \brief Add an arrow debug entity. - entt::entity addArrow(const Float4 &color = 0xf351ff_rgbaf); + std::tuple + addArrow(const Float4 &color = 0xEA2502FF_rgbaf); void update() { - for (auto &system : m_subsystems) { + for (auto [hash, system] : m_subsystems) { system->update(); } } @@ -125,4 +145,9 @@ struct DebugMeshComponent { Float3 scale = Float3::Ones(); }; +namespace gui { + void addDebugMesh(DebugMeshComponent &dmc, + bool enable_pipeline_switch = true); +} + } // namespace candlewick diff --git a/src/candlewick/core/DepthAndShadowPass.cpp b/src/candlewick/core/DepthAndShadowPass.cpp index a33b1326..af105bfd 100644 --- a/src/candlewick/core/DepthAndShadowPass.cpp +++ b/src/candlewick/core/DepthAndShadowPass.cpp @@ -8,33 +8,42 @@ namespace candlewick { -static SDL_GPUGraphicsPipeline * +static GraphicsPipeline create_depth_pass_pipeline(const Device &device, const MeshLayout &layout, SDL_GPUTextureFormat format, const DepthPass::Config config) { auto vertexShader = Shader::fromMetadata(device, "ShadowCast.vert"); auto fragmentShader = Shader::fromMetadata(device, "ShadowCast.frag"); - SDL_GPUGraphicsPipelineCreateInfo pipeline_desc{ - .vertex_shader = vertexShader, - .fragment_shader = fragmentShader, - .vertex_input_state = layout.toVertexInputState(), - .primitive_type = SDL_GPU_PRIMITIVETYPE_TRIANGLELIST, - .rasterizer_state{ - .fill_mode = SDL_GPU_FILLMODE_FILL, - .cull_mode = config.cull_mode, - .depth_bias_constant_factor = config.depth_bias_constant_factor, - .depth_bias_slope_factor = config.depth_bias_slope_factor, - .enable_depth_bias = config.enable_depth_bias, - .enable_depth_clip = config.enable_depth_clip}, - .depth_stencil_state{.compare_op = SDL_GPU_COMPAREOP_LESS, - .enable_depth_test = true, - .enable_depth_write = true}, - .target_info{.color_target_descriptions = nullptr, - .num_color_targets = 0, - .depth_stencil_format = format, - .has_depth_stencil_target = true}, - }; - return SDL_CreateGPUGraphicsPipeline(device, &pipeline_desc); + return GraphicsPipeline( + device, + { + .vertex_shader = vertexShader, + .fragment_shader = fragmentShader, + .vertex_input_state = layout.toVertexInputState(), + .primitive_type = SDL_GPU_PRIMITIVETYPE_TRIANGLELIST, + .rasterizer_state{ + .fill_mode = SDL_GPU_FILLMODE_FILL, + .cull_mode = config.cull_mode, + .depth_bias_constant_factor = config.depth_bias_constant_factor, + .depth_bias_slope_factor = config.depth_bias_slope_factor, + .enable_depth_bias = config.enable_depth_bias, + .enable_depth_clip = config.enable_depth_clip, + }, + .multisample_state{config.sample_count}, + .depth_stencil_state{ + .compare_op = SDL_GPU_COMPAREOP_LESS, + .enable_depth_test = true, + .enable_depth_write = true, + }, + .target_info{ + .color_target_descriptions = nullptr, + .num_color_targets = 0, + .depth_stencil_format = format, + .has_depth_stencil_target = true, + }, + .props = 0, + }, + config.pipeline_name); } DepthPass::DepthPass(const Device &device, const MeshLayout &layout, @@ -42,32 +51,23 @@ DepthPass::DepthPass(const Device &device, const MeshLayout &layout, const Config &config) : _device(device), depthTexture(depth_texture) { pipeline = create_depth_pass_pipeline(device, layout, format, config); - if (!pipeline) - terminate_with_message("Failed to create graphics pipeline: {:s}.", - SDL_GetError()); } -DepthPass::DepthPass(const Device &device, const MeshLayout &layout, - const Texture &texture, const Config &config) - : DepthPass(device, layout, texture, texture.format(), config) {} - DepthPass::DepthPass(DepthPass &&other) noexcept : _device(other._device) , depthTexture(other.depthTexture) - , pipeline(other.pipeline) { + , pipeline(std::move(other.pipeline)) { other._device = nullptr; other.depthTexture = nullptr; - other.pipeline = nullptr; } DepthPass &DepthPass::operator=(DepthPass &&other) noexcept { if (this != &other) { _device = other._device; depthTexture = other.depthTexture; - pipeline = other.pipeline; + pipeline = std::move(other.pipeline); other._device = nullptr; other.depthTexture = nullptr; - other.pipeline = nullptr; } return *this; } @@ -85,7 +85,7 @@ void DepthPass::render(CommandBuffer &command_buffer, const Mat4f &viewProj, SDL_GPURenderPass *render_pass = SDL_BeginGPURenderPass(command_buffer, nullptr, 0, &depth_info); - SDL_BindGPUGraphicsPipeline(render_pass, pipeline); + pipeline.bind(render_pass); for (auto &[mesh, tr] : castables) { assert(validateMesh(mesh)); @@ -105,15 +105,10 @@ gpuViewportFromAtlasRegion(const ShadowMapPass::AtlasRegion ®) { }; void DepthPass::release() noexcept { - if (_device) { - if (pipeline) { - SDL_ReleaseGPUGraphicsPipeline(_device, pipeline); - pipeline = nullptr; - } - if (depthTexture) - depthTexture = nullptr; + if (_device && depthTexture) { + depthTexture = nullptr; } - _device = nullptr; + pipeline.release(); } void ShadowMapPass::configureAtlasRegions(const Config &config) { @@ -134,7 +129,7 @@ void ShadowMapPass::configureAtlasRegions(const Config &config) { ShadowMapPass::ShadowMapPass(const Device &device, const MeshLayout &layout, SDL_GPUTextureFormat format, const Config &config) - : _device(device), _numLights(config.numLights) { + : m_device(device), m_numLights(config.numLights) { const Uint32 atlasWidth = config.numLights * config.width; const Uint32 atlasHeight = config.height; @@ -163,10 +158,9 @@ ShadowMapPass::ShadowMapPass(const Device &device, const MeshLayout &layout, config.depth_bias_slope_factor, config.enable_depth_bias, config.enable_depth_clip, + nullptr, + SDL_GPU_SAMPLECOUNT_1, }); - if (!pipeline) - terminate_with_message("Failed to create shadow cast pipeline: {:s}.", - SDL_GetError()); SDL_GPUSamplerCreateInfo sample_desc{ .min_filter = SDL_GPU_FILTER_LINEAR, @@ -182,46 +176,41 @@ ShadowMapPass::ShadowMapPass(const Device &device, const MeshLayout &layout, } ShadowMapPass::ShadowMapPass(ShadowMapPass &&other) noexcept - : _device(other._device) - , _numLights(other._numLights) + : m_device(other.m_device) + , m_numLights(other.m_numLights) , shadowMap(std::move(other.shadowMap)) - , pipeline(other.pipeline) + , pipeline(std::move(other.pipeline)) , sampler(other.sampler) , cam(std::move(other.cam)) , regions(std::move(other.regions)) { - other._device = nullptr; - other.pipeline = nullptr; + other.m_device = nullptr; other.sampler = nullptr; } ShadowMapPass &ShadowMapPass::operator=(ShadowMapPass &&other) noexcept { - _device = other._device; - _numLights = other._numLights; + m_device = other.m_device; + m_numLights = other.m_numLights; shadowMap = std::move(other.shadowMap); sampler = other.sampler; - pipeline = other.pipeline; + pipeline = std::move(other.pipeline); cam = std::move(other.cam); regions = std::move(other.regions); - other._device = nullptr; - other.pipeline = nullptr; + other.m_device = nullptr; other.sampler = nullptr; return *this; } void ShadowMapPass::release() noexcept { - if (_device) { + if (m_device) { if (sampler) { - SDL_ReleaseGPUSampler(_device, sampler); + SDL_ReleaseGPUSampler(m_device, sampler); } sampler = nullptr; - if (pipeline) { - SDL_ReleaseGPUGraphicsPipeline(_device, pipeline); - } - pipeline = nullptr; } + pipeline.release(); shadowMap.destroy(); - _device = nullptr; + m_device = nullptr; } void ShadowMapPass::render(CommandBuffer &command_buffer, @@ -237,7 +226,7 @@ void ShadowMapPass::render(CommandBuffer &command_buffer, SDL_GPURenderPass *render_pass = SDL_BeginGPURenderPass(command_buffer, nullptr, 0, &depth_info); - SDL_BindGPUGraphicsPipeline(render_pass, pipeline); + pipeline.bind(render_pass); for (size_t i = 0; i < numLights(); i++) { SDL_GPUViewport vp = gpuViewportFromAtlasRegion(regions[i]); diff --git a/src/candlewick/core/DepthAndShadowPass.h b/src/candlewick/core/DepthAndShadowPass.h index 1285fad8..7350ddec 100644 --- a/src/candlewick/core/DepthAndShadowPass.h +++ b/src/candlewick/core/DepthAndShadowPass.h @@ -26,6 +26,7 @@ #include "Tags.h" #include "Texture.h" #include "Camera.h" +#include "GraphicsPipeline.h" #include "math_types.h" #include "LightUniforms.h" @@ -56,10 +57,12 @@ class DepthPass { float depth_bias_slope_factor; bool enable_depth_bias; bool enable_depth_clip; + const char *pipeline_name; + SDL_GPUSampleCount sample_count; }; SDL_GPUTexture *depthTexture = nullptr; - SDL_GPUGraphicsPipeline *pipeline = nullptr; + GraphicsPipeline pipeline{NoInit}; DepthPass(NoInitT) {} @@ -86,7 +89,8 @@ class DepthPass { /// \param depth_texture %Texture object. /// \param config Configuration. DepthPass(const Device &device, const MeshLayout &layout, - const Texture &depth_texture, const Config &config = {}); + const Texture &texture, const Config &config = {}) + : DepthPass(device, layout, texture, texture.format(), config) {} DepthPass(const DepthPass &) = delete; DepthPass &operator=(const DepthPass &) = delete; @@ -98,6 +102,7 @@ class DepthPass { /// Release the pass resources. void release() noexcept; + /// Class destructor. ~DepthPass() noexcept { this->release(); } }; @@ -120,8 +125,8 @@ struct ShadowPassConfig { /// The user has to take care of setting the "cameras" corresponding to the /// actual lights. class ShadowMapPass { - SDL_GPUDevice *_device = nullptr; - Uint32 _numLights = 0; + SDL_GPUDevice *m_device = nullptr; + Uint32 m_numLights = 0; void configureAtlasRegions(const ShadowPassConfig &config); @@ -136,7 +141,7 @@ class ShadowMapPass { }; /// actually a texture atlas Texture shadowMap{NoInit}; - SDL_GPUGraphicsPipeline *pipeline = nullptr; + GraphicsPipeline pipeline{NoInit}; SDL_GPUSampler *sampler = nullptr; std::array cam; /// regions of the atlas @@ -157,7 +162,7 @@ class ShadowMapPass { ShadowMapPass &operator=(ShadowMapPass &&other) noexcept; bool initialized() const { - return (pipeline != nullptr) && (sampler != nullptr); + return pipeline.initialized() && (sampler != nullptr); } void render(CommandBuffer &cmdBuf, std::span castables); @@ -165,7 +170,7 @@ class ShadowMapPass { void release() noexcept; ~ShadowMapPass() noexcept { this->release(); } - auto numLights() const noexcept { return _numLights; } + auto numLights() const noexcept { return m_numLights; } }; /// \addtogroup depth_pass diff --git a/src/candlewick/core/Device.h b/src/candlewick/core/Device.h index c8a2cd29..a9b72e89 100644 --- a/src/candlewick/core/Device.h +++ b/src/candlewick/core/Device.h @@ -19,8 +19,20 @@ struct Device { explicit Device(NoInitT) noexcept; explicit Device(SDL_GPUShaderFormat format_flags, bool debug_mode = false); Device(const Device &) = delete; - Device(Device &&other) noexcept; - Device &operator=(Device &&) = delete; + Device(Device &&other) noexcept { + _device = other._device; + other._device = nullptr; + } + + Device &operator=(const Device &) = delete; + Device &operator=(Device &&other) noexcept { + if (this != &other) { + this->destroy(); + _device = other._device; + other._device = nullptr; + } + return *this; + } void create(SDL_GPUShaderFormat format_flags, bool debug_mode = false); @@ -51,9 +63,4 @@ struct Device { inline Device::Device(NoInitT) noexcept : _device(nullptr) {} -inline Device::Device(Device &&other) noexcept { - _device = other._device; - other._device = nullptr; -} - } // namespace candlewick diff --git a/src/candlewick/core/GraphicsPipeline.h b/src/candlewick/core/GraphicsPipeline.h new file mode 100644 index 00000000..6a07c6b5 --- /dev/null +++ b/src/candlewick/core/GraphicsPipeline.h @@ -0,0 +1,99 @@ +#pragma once + +#include "Core.h" +#include "Tags.h" +#include "errors.h" +#include +#include + +namespace candlewick { + +/// \brief Class representing a graphics pipeline. +/// +/// The GraphicsPipeline is a RAII wrapper around the \c SDL_GPUGraphicsPipeline +/// handle. +class GraphicsPipeline { + SDL_GPUDevice *m_device{nullptr}; + SDL_GPUGraphicsPipeline *m_pipeline{nullptr}; + struct PipelineMetadata { + SDL_GPUPrimitiveType primitiveType; + std::vector colorTargets; + SDL_GPUTextureFormat depthStencilFormat; + bool hasDepthStencilTarget; + SDL_GPUMultisampleState multisampleState; + + PipelineMetadata(SDL_GPUGraphicsPipelineCreateInfo desc) + : primitiveType(desc.primitive_type) + , colorTargets(desc.target_info.color_target_descriptions, + desc.target_info.color_target_descriptions + + desc.target_info.num_color_targets) + , depthStencilFormat(desc.target_info.depth_stencil_format) + , hasDepthStencilTarget(desc.target_info.depth_stencil_format) + , multisampleState(desc.multisample_state) {} + PipelineMetadata() noexcept {} + } m_meta; + +public: + GraphicsPipeline(NoInitT) {} + GraphicsPipeline(SDL_GPUDevice *device, + SDL_GPUGraphicsPipelineCreateInfo pipeline_desc, + const char *name) + : m_device(device), m_pipeline(nullptr), m_meta(pipeline_desc) { + if (name && !pipeline_desc.props) { + pipeline_desc.props = SDL_CreateProperties(); + SDL_SetStringProperty(pipeline_desc.props, + SDL_PROP_GPU_GRAPHICSPIPELINE_CREATE_NAME_STRING, + name); + } + m_pipeline = SDL_CreateGPUGraphicsPipeline(m_device, &pipeline_desc); + if (!m_pipeline) { + terminate_with_message("Failed to create graphics pipeline: {:s}", + SDL_GetError()); + } + } + + bool initialized() const noexcept { return m_pipeline; } + SDL_GPUGraphicsPipeline *handle() const noexcept { return m_pipeline; } + + GraphicsPipeline(const GraphicsPipeline &) = delete; + GraphicsPipeline(GraphicsPipeline &&other) noexcept + : m_device(other.m_device) + , m_pipeline(other.m_pipeline) + , m_meta(std::move(other.m_meta)) { + other.m_device = nullptr; + other.m_pipeline = nullptr; + } + + GraphicsPipeline &operator=(const GraphicsPipeline &) = delete; + GraphicsPipeline &operator=(GraphicsPipeline &&other) noexcept { + if (this != &other) { + this->release(); // release if we've got managed resources + m_device = other.m_device; + m_pipeline = other.m_pipeline; + m_meta = std::move(other.m_meta); + + other.m_device = nullptr; + other.m_pipeline = nullptr; + } + return *this; + } + + auto primitiveType() const noexcept { return m_meta.primitiveType; } + auto numColorTargets() const noexcept { + return Uint32(m_meta.colorTargets.size()); + } + + void bind(SDL_GPURenderPass *render_pass) const noexcept { + SDL_BindGPUGraphicsPipeline(render_pass, m_pipeline); + } + + void release() noexcept { + if (m_device && m_pipeline) + SDL_ReleaseGPUGraphicsPipeline(m_device, m_pipeline); + m_pipeline = nullptr; + } + + ~GraphicsPipeline() noexcept { this->release(); } +}; + +} // namespace candlewick diff --git a/src/candlewick/core/GuiSystem.cpp b/src/candlewick/core/GuiSystem.cpp index 1ec89994..94ec447e 100644 --- a/src/candlewick/core/GuiSystem.cpp +++ b/src/candlewick/core/GuiSystem.cpp @@ -11,14 +11,14 @@ namespace candlewick { GuiSystem::GuiSystem(const RenderContext &renderer, GuiBehavior behav) - : m_renderer(&renderer), _callbacks{behav} { - if (!init(renderer)) { + : m_renderer(&renderer), m_callback{std::move(behav)} { + if (!init()) { terminate_with_message("Failed to initialize ImGui for SDLGPU3."); } + m_initialized = true; } -bool GuiSystem::init(const RenderContext &renderer) { - m_renderer = &renderer; +bool GuiSystem::init() { assert(!m_initialized); // can't initialize twice IMGUI_CHECKVERSION(); ImGui::CreateContext(); @@ -36,16 +36,15 @@ bool GuiSystem::init(const RenderContext &renderer) { ImGui::StyleColorsDark(&style); style.WindowBorderSize = 0.5f; style.WindowRounding = 6; - if (!ImGui_ImplSDL3_InitForSDLGPU(renderer.window)) { + if (!ImGui_ImplSDL3_InitForSDLGPU(m_renderer->window)) { return false; } ImGui_ImplSDLGPU3_InitInfo imguiInfo{ - .Device = renderer.device, - .ColorTargetFormat = renderer.getSwapchainTextureFormat(), + .Device = m_renderer->device, + .ColorTargetFormat = m_renderer->getSwapchainTextureFormat(), .MSAASamples = SDL_GPU_SAMPLECOUNT_1, }; - m_initialized = ImGui_ImplSDLGPU3_Init(&imguiInfo); - return m_initialized; + return ImGui_ImplSDLGPU3_Init(&imguiInfo); } void GuiSystem::render(CommandBuffer &cmdBuf) { @@ -53,16 +52,14 @@ void GuiSystem::render(CommandBuffer &cmdBuf) { ImGui_ImplSDL3_NewFrame(); ImGui::NewFrame(); - for (auto &cb : _callbacks) { - cb(*m_renderer); - } + m_callback(*m_renderer); ImGui::Render(); ImDrawData *draw_data = ImGui::GetDrawData(); ImGui_ImplSDLGPU3_PrepareDrawData(draw_data, cmdBuf); SDL_GPUColorTargetInfo info{ - .texture = m_renderer->swapchain, + .texture = m_renderer->resolvedColorTarget(), .clear_color{}, .load_op = SDL_GPU_LOADOP_LOAD, .store_op = SDL_GPU_STOREOP_STORE, @@ -74,70 +71,74 @@ void GuiSystem::render(CommandBuffer &cmdBuf) { } void GuiSystem::release() { - ImGui_ImplSDL3_Shutdown(); - ImGui_ImplSDLGPU3_Shutdown(); - ImGui::DestroyContext(); - m_renderer = nullptr; + if (m_initialized) { + ImGui_ImplSDL3_Shutdown(); + ImGui_ImplSDLGPU3_Shutdown(); + ImGui::DestroyContext(); + m_renderer = nullptr; + } } -void showCandlewickAboutWindow(bool *p_open, float wrap_width) { - if (!ImGui::Begin("About Candlewick", p_open, - ImGuiWindowFlags_AlwaysAutoResize)) { +namespace gui { + void showCandlewickAboutWindow(bool *p_open, float wrap_width) { + if (!ImGui::Begin("About Candlewick", p_open, + ImGuiWindowFlags_AlwaysAutoResize)) { + ImGui::End(); + return; + } + + ImGui::Text("Candlewick v%s", CANDLEWICK_VERSION); + ImGui::Spacing(); + + ImGui::TextLinkOpenURL("Homepage", + "https://github.com/Simple-Robotics/candlewick/"); + ImGui::SameLine(); + ImGui::TextLinkOpenURL( + "Releases", "https://github.com/Simple-Robotics/candlewick/releases"); + + ImGui::Separator(); + ImGui::Text("Copyright (c) 2024-2025 Inria"); + ImGui::PushTextWrapPos(ImGui::GetCursorPosX() + wrap_width); + ImGui::Text("Candlewick is licensed under the BSD 2-Clause License, see " + "LICENSE file for more information."); + ImGui::PopTextWrapPos(); + ImGui::End(); - return; } - ImGui::Text("Candlewick v%s", CANDLEWICK_VERSION); - ImGui::Spacing(); - - ImGui::TextLinkOpenURL("Homepage", - "https://github.com/Simple-Robotics/candlewick/"); - ImGui::SameLine(); - ImGui::TextLinkOpenURL( - "Releases", "https://github.com/Simple-Robotics/candlewick/releases"); - - ImGui::Separator(); - ImGui::Text("Copyright (c) 2024-2025 Inria"); - ImGui::PushTextWrapPos(ImGui::GetCursorPosX() + wrap_width); - ImGui::Text("Candlewick is licensed under the BSD 2-Clause License, see " - "LICENSE file for more information."); - ImGui::PopTextWrapPos(); - - ImGui::End(); -} - -void guiAddLightControls(DirectionalLight &light) { - ImGui::SliderFloat("intensity", &light.intensity, 0.1f, 10.f); - ImGui::DragFloat3("direction", light.direction.data(), 0.0f, -1.f, 1.f); - light.direction.stableNormalize(); - ImGui::ColorEdit3("color", light.color.data()); -} + void addLightControls(DirectionalLight &light) { + ImGui::SliderFloat("intensity", &light.intensity, 0.1f, 10.f); + ImGui::DragFloat3("direction", light.direction.data(), 0.0f, -1.f, 1.f); + light.direction.stableNormalize(); + ImGui::ColorEdit3("color", light.color.data()); + } -void guiAddLightControls(std::span lights) { - Uint16 i = 0; - char label[32]; - for (auto &light : lights) { - SDL_snprintf(label, 32, "light###%d", i); - ImGui::PushID(label); - ImGui::Bullet(); - ImGui::Indent(); - guiAddLightControls(light); - ImGui::Unindent(); - ImGui::PopID(); - i++; + void addLightControls(std::span lights) { + Uint16 i = 0; + char label[32]; + for (auto &light : lights) { + SDL_snprintf(label, 32, "light###%d", i); + ImGui::PushID(label); + ImGui::Bullet(); + ImGui::Indent(); + addLightControls(light); + ImGui::Unindent(); + ImGui::PopID(); + i++; + } } -} -void guiAddLightControls(std::span lights, Uint32 numLights, - Uint32 start) { - guiAddLightControls(lights.subspan(start, numLights)); -} + void addLightControls(std::span lights, Uint32 numLights, + Uint32 start) { + addLightControls(lights.subspan(start, numLights)); + } -void guiAddDisableCheckbox(const char *label, entt::registry ®, - entt::entity id, bool &flag) { - if (ImGui::Checkbox(label, &flag)) { - toggleDisable(reg, id, flag); + void addDisableCheckbox(const char *label, entt::registry ®, + entt::entity id, bool &flag) { + if (ImGui::Checkbox(label, &flag)) { + toggleDisable(reg, id, flag); + } } -} +} // namespace gui } // namespace candlewick diff --git a/src/candlewick/core/GuiSystem.h b/src/candlewick/core/GuiSystem.h index 921eccae..01ecf415 100644 --- a/src/candlewick/core/GuiSystem.h +++ b/src/candlewick/core/GuiSystem.h @@ -13,60 +13,61 @@ namespace candlewick { class GuiSystem { RenderContext const *m_renderer; bool m_initialized = false; + std::function m_callback; + + bool init(); public: using GuiBehavior = std::function; GuiSystem(const RenderContext &renderer, GuiBehavior behav); - - void addCallback(GuiBehavior cb) { _callbacks.push_back(std::move(cb)); } + GuiSystem(GuiSystem &&other) noexcept + : m_renderer{other.m_renderer} + , m_initialized(other.m_initialized) + , m_callback(std::move(other.m_callback)) { + other.m_renderer = nullptr; + other.m_initialized = false; + } void render(CommandBuffer &cmdBuf); void release(); bool initialized() const { return m_initialized; } - - std::vector _callbacks; - -private: - bool init(const RenderContext &renderer); }; -/// \ingroup gui_util -/// \{ -/// \brief Show an about window providing information about Candlewick. -void showCandlewickAboutWindow(bool *p_open = NULL, float wrap_width = 400.f); - -struct DirectionalLight; - -/// \brief Adds a set of ImGui elements to control a DirectionalLight. -void guiAddLightControls(DirectionalLight &light); - -/// \brief Add controls for multiple lights. -void guiAddLightControls(std::span lights); - -void guiAddLightControls(std::span lights, Uint32 numLights, - Uint32 start = 0); - -/// \brief Adds an ImGui::Checkbox which toggles the Disable component on the -/// entity. -void guiAddDisableCheckbox(const char *label, entt::registry ®, - entt::entity id, bool &flag); - enum class DialogFileType { IMAGES, VIDEOS }; -/// \brief Add a GUI button-text pair to select a file to save something to. -/// -/// This function can only be called from the main thread. -void guiAddFileDialog(SDL_Window *window, DialogFileType dialog_file_type, - std::string &filename); -/// \} - /// \brief Set input/output string to a generated filename computed from a /// timestamp. void generateMediaFilenameFromTimestamp( const char *prefix, std::string &out, const char *extension = ".png", DialogFileType file_type = DialogFileType::IMAGES); +/// \brief GUI utilities. +namespace gui { + /// \brief Show an about window providing information about Candlewick. + void showCandlewickAboutWindow(bool *p_open = NULL, float wrap_width = 400.f); + + /// \brief Adds a set of ImGui elements to control a DirectionalLight. + void addLightControls(DirectionalLight &light); + + /// \brief Add controls for multiple lights. + void addLightControls(std::span lights); + + void addLightControls(std::span lights, Uint32 numLights, + Uint32 start = 0); + + /// \brief Adds an ImGui::Checkbox which toggles the Disable component on the + /// entity. + void addDisableCheckbox(const char *label, entt::registry ®, + entt::entity id, bool &flag); + + /// \brief Add a GUI button-text pair to select a file to save something to. + /// + /// This function can only be called from the main thread. + void addFileDialog(SDL_Window *window, DialogFileType dialog_file_type, + std::string &filename); +} // namespace gui + } // namespace candlewick diff --git a/src/candlewick/multibody/LoadCoalGeometries.cpp b/src/candlewick/core/LoadCoalGeometries.cpp similarity index 98% rename from src/candlewick/multibody/LoadCoalGeometries.cpp rename to src/candlewick/core/LoadCoalGeometries.cpp index e2e14a4f..223aad44 100644 --- a/src/candlewick/multibody/LoadCoalGeometries.cpp +++ b/src/candlewick/core/LoadCoalGeometries.cpp @@ -145,7 +145,7 @@ MeshData loadCoalPrimitive(const coal::ShapeBase &geometry) { float d; getPlaneOrHalfspaceNormalOffset(geometry, n, d); const auto quat = Eigen::Quaternionf::FromTwoVectors(Float3::UnitZ(), n); - transform.scale(kPlaneScale).rotate(quat).translate(d * Float3::UnitZ()); + transform.rotate(quat).translate(d * Float3::UnitZ()).scale(kPlaneScale); break; } default: diff --git a/src/candlewick/multibody/LoadCoalGeometries.h b/src/candlewick/core/LoadCoalGeometries.h similarity index 100% rename from src/candlewick/multibody/LoadCoalGeometries.h rename to src/candlewick/core/LoadCoalGeometries.h diff --git a/src/candlewick/core/RenderContext.cpp b/src/candlewick/core/RenderContext.cpp index e697d282..62de333f 100644 --- a/src/candlewick/core/RenderContext.cpp +++ b/src/candlewick/core/RenderContext.cpp @@ -7,41 +7,37 @@ #include namespace candlewick { -RenderContext::RenderContext(Device &&device_, Window &&window_) - : device(std::move(device_)) - , window(std::move(window_)) - , swapchain(nullptr) { +RenderContext::RenderContext(Device &&device_, Window &&window_, + SDL_GPUTextureFormat suggested_depth_format) + : device(std::move(device_)), window(std::move(window_)) { if (!SDL_ClaimWindowForGPUDevice(device, window)) throw RAIIException(SDL_GetError()); -} -RenderContext::RenderContext(Device &&device_, Window &&window_, - SDL_GPUTextureFormat suggested_depth_format) - : RenderContext(std::move(device_), std::move(window_)) { - createDepthTexture(suggested_depth_format); + createRenderTargets(suggested_depth_format); } bool RenderContext::waitAndAcquireSwapchain(CommandBuffer &command_buffer) { - assert(SDL_IsMainThread()); + CANDLEWICK_ASSERT(SDL_IsMainThread(), + "Can only acquire swapchain from main thread."); return SDL_WaitAndAcquireGPUSwapchainTexture(command_buffer, window, &swapchain, NULL, NULL); } bool RenderContext::acquireSwapchain(CommandBuffer &command_buffer) { - assert(SDL_IsMainThread()); + CANDLEWICK_ASSERT(SDL_IsMainThread(), + "Can only acquire swapchain from main thread."); return SDL_AcquireGPUSwapchainTexture(command_buffer, window, &swapchain, NULL, NULL); } -void RenderContext::createDepthTexture( +void RenderContext::createRenderTargets( SDL_GPUTextureFormat suggested_depth_format) { - auto [width, height] = window.size(); + auto [width, height] = window.sizeInPixels(); - SDL_GPUTextureCreateInfo texInfo{ + SDL_GPUTextureCreateInfo colorInfo{ .type = SDL_GPU_TEXTURETYPE_2D, - .format = suggested_depth_format, - .usage = SDL_GPU_TEXTUREUSAGE_DEPTH_STENCIL_TARGET | - SDL_GPU_TEXTUREUSAGE_SAMPLER, + .format = getSwapchainTextureFormat(), + .usage = SDL_GPU_TEXTUREUSAGE_COLOR_TARGET | SDL_GPU_TEXTUREUSAGE_SAMPLER, .width = Uint32(width), .height = Uint32(height), .layer_count_or_depth = 1, @@ -49,30 +45,95 @@ void RenderContext::createDepthTexture( .sample_count = SDL_GPU_SAMPLECOUNT_1, .props = 0, }; + colorBuffer = Texture(device, colorInfo, "Main color target"); + + if (suggested_depth_format == SDL_GPU_TEXTUREFORMAT_INVALID) + return; + + SDL_GPUTextureCreateInfo depthInfo = colorInfo; + depthInfo.format = suggested_depth_format; + depthInfo.usage = + SDL_GPU_TEXTUREUSAGE_DEPTH_STENCIL_TARGET | SDL_GPU_TEXTUREUSAGE_SAMPLER; + SDL_GPUTextureFormat depth_format_fallbacks[] = { // supported on macOS, supports SAMPLER usage SDL_GPU_TEXTUREFORMAT_D16_UNORM, // not sure about SAMPLER usage on macOS SDL_GPU_TEXTUREFORMAT_D32_FLOAT, }; + size_t try_idx = 0; - while (!SDL_GPUTextureSupportsFormat(device, texInfo.format, texInfo.type, - texInfo.usage) && + while (!SDL_GPUTextureSupportsFormat(device, depthInfo.format, depthInfo.type, + depthInfo.usage) && try_idx < std::size(depth_format_fallbacks)) { - texInfo.format = depth_format_fallbacks[try_idx]; + depthInfo.format = depth_format_fallbacks[try_idx]; try_idx++; } - depth_texture = Texture(this->device, texInfo); + + depthBuffer = Texture(this->device, depthInfo, "Main depth target"); SDL_Log("Created depth texture of format %s, size %d x %d\n", - magic_enum::enum_name(texInfo.format).data(), width, height); - SDL_SetGPUTextureName(device, depth_texture, "Main depth texture"); + magic_enum::enum_name(depthInfo.format).data(), width, height); +} + +void RenderContext::createMsaaTargets(SDL_GPUSampleCount samples) { + auto [width, height] = window.sizeInPixels(); + + SDL_GPUTextureCreateInfo msaaColorInfo{ + .type = SDL_GPU_TEXTURETYPE_2D, + .format = colorBuffer.format(), + .usage = SDL_GPU_TEXTUREUSAGE_COLOR_TARGET, + .width = Uint32(width), + .height = Uint32(height), + .layer_count_or_depth = 1, + .num_levels = 1, + .sample_count = samples, + .props = 0, + }; + if (!SDL_GPUTextureSupportsSampleCount(device, colorBuffer.format(), + samples)) { + terminate_with_message("Unsupported sample count for MSAA color target."); + } + colorMsaa = Texture(device, msaaColorInfo, "MSAA color target"); + + if (hasDepthTexture()) { + // overwrite current depth texture to make it Msaa + auto depthInfo = depthBuffer.description(); + depthInfo.sample_count = samples; + depthBuffer = Texture(device, depthInfo, "Main depth target [MSAA]"); + } +} + +void RenderContext::presentToSwapchain(CommandBuffer &command_buffer) { + // NOTE: we always present the resolved color buffer (whether MSAA or not) + auto [w, h] = window.sizeInPixels(); + + SDL_GPUBlitInfo blit{ + .source = colorBuffer.blitRegion(0, 0), + .destination{ + .texture = swapchain, + .mip_level = 0, + .layer_or_depth_plane = 0, + .x = 0, + .y = 0, + .w = Uint32(w), + .h = Uint32(h), + }, + .load_op = SDL_GPU_LOADOP_DONT_CARE, + .clear_color = {}, + .flip_mode = SDL_FLIP_NONE, + .filter = SDL_GPU_FILTER_LINEAR, + .cycle = false, + }; + SDL_BlitGPUTexture(command_buffer, &blit); } -RenderContext::~RenderContext() noexcept { +void RenderContext::destroy() noexcept { if (device && window) { SDL_ReleaseWindowFromGPUDevice(device, window); } - depth_texture.destroy(); + colorMsaa.destroy(); + colorBuffer.destroy(); + depthBuffer.destroy(); window.destroy(); device.destroy(); } @@ -135,9 +196,11 @@ namespace rend { #endif for (auto &view : meshViews) { #ifndef NDEBUG - SDL_assert(ib == view.indexBuffer); + CANDLEWICK_ASSERT(ib == view.indexBuffer, + "Invalid view set (different index buffers)"); for (size_t i = 0; i < n_vbs; i++) - SDL_assert(vbs[i] == view.vertexBuffers[i]); + CANDLEWICK_ASSERT(vbs[i] == view.vertexBuffers[i], + "Invalid view set (different vertex buffers)"); #endif drawView(pass, view, numInstances); } diff --git a/src/candlewick/core/RenderContext.h b/src/candlewick/core/RenderContext.h index acf53b17..85b1ce9f 100644 --- a/src/candlewick/core/RenderContext.h +++ b/src/candlewick/core/RenderContext.h @@ -1,3 +1,4 @@ +/// \copyright Copyright (c) 2025 Inria #pragma once #include "Device.h" @@ -11,6 +12,21 @@ namespace candlewick { +inline constexpr int sdlSampleToValue(SDL_GPUSampleCount samples) { + switch (samples) { + case SDL_GPU_SAMPLECOUNT_1: + return 1; + case SDL_GPU_SAMPLECOUNT_2: + return 2; + case SDL_GPU_SAMPLECOUNT_4: + return 4; + case SDL_GPU_SAMPLECOUNT_8: + return 8; + default: + return 0; + } +} + /// \brief The RenderContext class provides a rendering context for a graphical /// application. /// @@ -18,22 +34,67 @@ namespace candlewick { /// \sa Device /// \sa Mesh struct RenderContext { +private: + Texture colorMsaa{NoInit}; + Texture colorBuffer{NoInit}; // no MSAA + Texture depthBuffer{NoInit}; // no MSAA + bool m_msaaEnabled = false; + SDL_GPUTexture *swapchain{nullptr}; + + void createMsaaTargets(SDL_GPUSampleCount samples); + void createRenderTargets(SDL_GPUTextureFormat suggested_depth_format); + +public: Device device; Window window; - SDL_GPUTexture *swapchain; - Texture depth_texture{NoInit}; - - RenderContext(NoInitT) - : device(NoInit), window(nullptr), swapchain(nullptr) {} - /// \brief Constructor without a depth format. - RenderContext(Device &&device, Window &&window); - /// \brief Constructor with a depth format. This will create a depth texture. + + RenderContext(NoInitT) : device(NoInit), window(nullptr) {} + /// \brief Constructor. + /// The last argument (the depth texture format) is optional. By default, it + /// is set to INVALID, and no depth texture is created. RenderContext(Device &&device, Window &&window, - SDL_GPUTextureFormat suggested_depth_format); + SDL_GPUTextureFormat suggested_depth_format = + SDL_GPU_TEXTUREFORMAT_INVALID); + + RenderContext(RenderContext &&) noexcept = default; + RenderContext &operator=(RenderContext &&) noexcept = default; + + const Texture &colorTarget() const { + return (m_msaaEnabled && colorMsaa) ? colorMsaa : colorBuffer; + } + + const Texture &depthTarget() const { return depthBuffer; } - /// \brief Add a depth texture to the rendering context. - /// \see hasDepthTexture() - void createDepthTexture(SDL_GPUTextureFormat suggested_depth_format); + const Texture &resolvedColorTarget() const { return colorBuffer; } + + bool msaaEnabled() const { return m_msaaEnabled; } + + SDL_GPUSampleCount getMsaaSampleCount() const { + if (m_msaaEnabled && colorMsaa) { + return colorMsaa.sampleCount(); + } + return SDL_GPU_SAMPLECOUNT_1; + } + + void enableMSAA(SDL_GPUSampleCount samples) { + if (samples > SDL_GPU_SAMPLECOUNT_1) { + m_msaaEnabled = true; + createMsaaTargets(samples); + if (int sample_size = sdlSampleToValue(samples)) { + SDL_Log("MSAA enabled with %d samples", sample_size); + } else { + terminate_with_message("Unrecognized sample count {:d}", sample_size); + } + } else { + disableMSAA(); + } + } + + void disableMSAA() { + m_msaaEnabled = false; + colorMsaa.destroy(); + SDL_Log("MSAA disabled."); + } bool initialized() const { return bool(device); } @@ -49,25 +110,30 @@ struct RenderContext { /// the meaning of "main thread"). bool acquireSwapchain(CommandBuffer &command_buffer); + /// \brief Wait for the swapchain to be available. bool waitForSwapchain() { return SDL_WaitForGPUSwapchain(device, window); } + /// \brief Present the resolved texture to the swapchain. You must acquire the + /// swapchain beforehand. + void presentToSwapchain(CommandBuffer &command_buffer); + SDL_GPUTextureFormat getSwapchainTextureFormat() const { return SDL_GetGPUSwapchainTextureFormat(device, window); } /// \brief Check if a depth texture was created. - inline bool hasDepthTexture() const { return depth_texture.hasValue(); } + inline bool hasDepthTexture() const { return depthBuffer; } inline void setSwapchainParameters(SDL_GPUSwapchainComposition composition, SDL_GPUPresentMode present_mode) { SDL_SetGPUSwapchainParameters(device, window, composition, present_mode); } - SDL_GPUTextureFormat depthFormat() const { return depth_texture.format(); } - - ~RenderContext() noexcept; + SDL_GPUTextureFormat colorFormat() const { return colorBuffer.format(); } + SDL_GPUTextureFormat depthFormat() const { return depthBuffer.format(); } - void destroy() noexcept { this->~RenderContext(); } + void destroy() noexcept; + ~RenderContext() noexcept { this->destroy(); } }; namespace rend { diff --git a/src/candlewick/core/Scene.h b/src/candlewick/core/Scene.h deleted file mode 100644 index c140be47..00000000 --- a/src/candlewick/core/Scene.h +++ /dev/null @@ -1,17 +0,0 @@ -#pragma once - -#include "Core.h" -#include - -namespace candlewick { - -/// \brief The Scene concept requires that there exist functions to render the -/// scene. Provided a command buffer and Camera, and that there is a function to -/// release the resources of the Scene. -template -concept Scene = requires(T t, CommandBuffer &cmdBuf, const Camera &camera) { - { t.render(cmdBuf, camera) } -> std::same_as; - { t.release() } -> std::same_as; -}; - -} // namespace candlewick diff --git a/src/candlewick/core/Texture.cpp b/src/candlewick/core/Texture.cpp index 1ae1563a..baa86850 100644 --- a/src/candlewick/core/Texture.cpp +++ b/src/candlewick/core/Texture.cpp @@ -9,36 +9,38 @@ namespace candlewick { Texture::Texture(const Device &device, SDL_GPUTextureCreateInfo texture_desc, const char *name) - : _device(device) - , _texture(nullptr) - , _description(std::move(texture_desc)) { - if (!(_texture = SDL_CreateGPUTexture(_device, &_description))) { + : m_device(device) + , m_texture(nullptr) + , m_description(std::move(texture_desc)) { + if (!(m_texture = SDL_CreateGPUTexture(m_device, &m_description))) { std::string msg = std::format("Failed to create texture with format (%s)", - magic_enum::enum_name(_description.format)); + magic_enum::enum_name(m_description.format)); if (name) msg += std::format(" (name %s)", name); throw RAIIException(std::move(msg)); } if (name != nullptr) - SDL_SetGPUTextureName(_device, _texture, name); + SDL_SetGPUTextureName(m_device, m_texture, name); } Texture::Texture(Texture &&other) noexcept - : _device(other._device) - , _texture(other._texture) - , _description(std::move(other._description)) { - other._device = nullptr; - other._texture = nullptr; + : m_device(other.m_device) + , m_texture(other.m_texture) + , m_description(std::move(other.m_description)) { + other.m_device = nullptr; + other.m_texture = nullptr; } Texture &Texture::operator=(Texture &&other) noexcept { - this->destroy(); - _device = other._device; - _texture = other._texture; - _description = std::move(other._description); + if (this != &other) { + this->destroy(); + m_device = other.m_device; + m_texture = other.m_texture; + m_description = std::move(other.m_description); - other._device = nullptr; - other._texture = nullptr; + other.m_device = nullptr; + other.m_texture = nullptr; + } return *this; } @@ -47,7 +49,7 @@ SDL_GPUBlitRegion Texture::blitRegion(Uint32 x, Uint32 y, CANDLEWICK_ASSERT(layer_or_depth_plane < layerCount(), "layer is higher than layerCount!"); return { - .texture = _texture, + .texture = m_texture, .mip_level = 0, .layer_or_depth_plane = layer_or_depth_plane, .x = x, @@ -63,10 +65,10 @@ Uint32 Texture::textureSize() const { } void Texture::destroy() noexcept { - if (_device && _texture) { - SDL_ReleaseGPUTexture(_device, _texture); - _texture = nullptr; - _device = nullptr; + if (m_device && m_texture) { + SDL_ReleaseGPUTexture(m_device, m_texture); + m_texture = nullptr; + m_device = nullptr; } } } // namespace candlewick diff --git a/src/candlewick/core/Texture.h b/src/candlewick/core/Texture.h index aec4ec07..4721a155 100644 --- a/src/candlewick/core/Texture.h +++ b/src/candlewick/core/Texture.h @@ -7,9 +7,9 @@ namespace candlewick { class Texture { - SDL_GPUDevice *_device = nullptr; - SDL_GPUTexture *_texture = nullptr; - SDL_GPUTextureCreateInfo _description; + SDL_GPUDevice *m_device = nullptr; + SDL_GPUTexture *m_texture = nullptr; + SDL_GPUTextureCreateInfo m_description; public: Texture(NoInitT) {} @@ -21,23 +21,29 @@ class Texture { Texture(Texture &&other) noexcept; Texture &operator=(Texture &&other) noexcept; - operator SDL_GPUTexture *() const noexcept { return _texture; } + operator SDL_GPUTexture *() const noexcept { return m_texture; } - bool hasValue() const { return bool(_texture); } - const auto &description() const { return _description; } - SDL_GPUTextureType type() const { return _description.type; } - SDL_GPUTextureFormat format() const { return _description.format; } - SDL_GPUTextureUsageFlags usage() const { return _description.usage; } - Uint32 width() const { return _description.width; } - Uint32 height() const { return _description.height; } - Uint32 depth() const { return _description.layer_count_or_depth; } - Uint32 layerCount() const { return _description.layer_count_or_depth; } + bool operator==(const Texture &other) const noexcept { + return m_texture == other.m_texture; + } + + const auto &description() const { return m_description; } + SDL_GPUTextureType type() const { return m_description.type; } + SDL_GPUTextureFormat format() const { return m_description.format; } + SDL_GPUTextureUsageFlags usage() const { return m_description.usage; } + Uint32 width() const { return m_description.width; } + Uint32 height() const { return m_description.height; } + Uint32 depth() const { return m_description.layer_count_or_depth; } + Uint32 layerCount() const { return m_description.layer_count_or_depth; } + SDL_GPUSampleCount sampleCount() const { return m_description.sample_count; } SDL_GPUBlitRegion blitRegion(Uint32 offset_x, Uint32 y_offset, Uint32 layer_or_depth_plane = 0) const; Uint32 textureSize() const; + SDL_GPUDevice *device() const { return m_device; } + void destroy() noexcept; ~Texture() noexcept { this->destroy(); } }; diff --git a/src/candlewick/core/Window.h b/src/candlewick/core/Window.h index 10cbff12..329943ad 100644 --- a/src/candlewick/core/Window.h +++ b/src/candlewick/core/Window.h @@ -22,9 +22,11 @@ struct Window { } Window &operator=(Window &&other) noexcept { - this->~Window(); - _handle = other._handle; - other._handle = nullptr; + if (this != &other) { + this->destroy(); + _handle = other._handle; + other._handle = nullptr; + } return *this; } @@ -78,8 +80,7 @@ inline std::array Window::size() const { inline std::array Window::sizeInPixels() const { int width, height; - if (!SDL_GetWindowSizeInPixels(_handle, &width, &height)) { - } + SDL_GetWindowSizeInPixels(_handle, &width, &height); return {width, height}; } diff --git a/src/candlewick/core/debug/DepthViz.cpp b/src/candlewick/core/debug/DepthViz.cpp index b88c3799..512ec25c 100644 --- a/src/candlewick/core/debug/DepthViz.cpp +++ b/src/candlewick/core/debug/DepthViz.cpp @@ -1,7 +1,6 @@ #include "DepthViz.h" #include "../RenderContext.h" #include "../Shader.h" -#include namespace candlewick { @@ -42,15 +41,11 @@ DepthDebugPass DepthDebugPass::create(const RenderContext &renderer, .num_color_targets = 1, .has_depth_stencil_target = false}; - SDL_GPUGraphicsPipeline *pipeline = - SDL_CreateGPUGraphicsPipeline(device, &pipeline_desc); - if (!pipeline) { - auto msg = std::format("Failed to create depth debug pipeline: %s", - SDL_GetError()); - throw std::runtime_error(msg); - } - - return {depthTexture, sampler, pipeline}; + return { + depthTexture, + sampler, + GraphicsPipeline(device, pipeline_desc, "Depth debug"), + }; } struct alignas(16) cam_param_ubo_t { @@ -65,14 +60,14 @@ void renderDepthDebug(const RenderContext &renderer, const DepthDebugPass::Options &opts) { SDL_GPUColorTargetInfo color_target; SDL_zero(color_target); - color_target.texture = renderer.swapchain; + color_target.texture = renderer.colorTarget(); color_target.clear_color = {0., 0., 0., 1.}; color_target.load_op = SDL_GPU_LOADOP_CLEAR; color_target.store_op = SDL_GPU_STOREOP_STORE; SDL_GPURenderPass *render_pass = SDL_BeginGPURenderPass(command_buffer, &color_target, 1, nullptr); - SDL_BindGPUGraphicsPipeline(render_pass, pass.pipeline); + pass.pipeline.bind(render_pass); rend::bindFragmentSamplers(render_pass, 0, {{ diff --git a/src/candlewick/core/debug/DepthViz.h b/src/candlewick/core/debug/DepthViz.h index ebe420c4..8932099f 100644 --- a/src/candlewick/core/debug/DepthViz.h +++ b/src/candlewick/core/debug/DepthViz.h @@ -1,8 +1,8 @@ #pragma once -#include "../Core.h" -#include +#include "../GraphicsPipeline.h" #include "../Camera.h" +#include namespace candlewick { @@ -16,7 +16,7 @@ struct DepthDebugPass { }; SDL_GPUTexture *depthTexture; SDL_GPUSampler *sampler; - SDL_GPUGraphicsPipeline *pipeline; + GraphicsPipeline pipeline{NoInit}; static DepthDebugPass create(const RenderContext &renderer, SDL_GPUTexture *depthTexture); @@ -29,10 +29,7 @@ inline void DepthDebugPass::release(SDL_GPUDevice *device) { SDL_ReleaseGPUSampler(device, sampler); sampler = NULL; } - if (pipeline) { - SDL_ReleaseGPUGraphicsPipeline(device, pipeline); - pipeline = NULL; - } + pipeline.release(); } void renderDepthDebug(const RenderContext &renderer, CommandBuffer &cmdBuf, diff --git a/src/candlewick/core/debug/Frustum.cpp b/src/candlewick/core/debug/Frustum.cpp index b3a43de7..8afc26f9 100644 --- a/src/candlewick/core/debug/Frustum.cpp +++ b/src/candlewick/core/debug/Frustum.cpp @@ -7,8 +7,7 @@ namespace candlewick { namespace frustum_debug { - SDL_GPUGraphicsPipeline * - createFrustumDebugPipeline(const RenderContext &renderer) { + GraphicsPipeline createFrustumDebugPipeline(const RenderContext &renderer) { const auto &device = renderer.device; auto vertexShader = Shader::fromMetadata(device, "FrustumDebug.vert"); auto fragmentShader = Shader::fromMetadata(device, "VertexColor.frag"); @@ -17,19 +16,25 @@ namespace frustum_debug { SDL_zero(color_target); color_target.format = renderer.getSwapchainTextureFormat(); - SDL_GPUGraphicsPipelineCreateInfo info{ - .vertex_shader = vertexShader, - .fragment_shader = fragmentShader, - .primitive_type = SDL_GPU_PRIMITIVETYPE_LINELIST, - .depth_stencil_state{.compare_op = SDL_GPU_COMPAREOP_LESS_OR_EQUAL, - .enable_depth_test = true, - .enable_depth_write = true}, - .target_info{.color_target_descriptions = &color_target, - .num_color_targets = 1, - .depth_stencil_format = renderer.depthFormat(), - .has_depth_stencil_target = true}, - }; - return SDL_CreateGPUGraphicsPipeline(device, &info); + return GraphicsPipeline( + device, + { + .vertex_shader = vertexShader, + .fragment_shader = fragmentShader, + .primitive_type = SDL_GPU_PRIMITIVETYPE_LINELIST, + .depth_stencil_state{ + .compare_op = SDL_GPU_COMPAREOP_LESS_OR_EQUAL, + .enable_depth_test = true, + .enable_depth_write = true, + }, + .target_info{ + .color_target_descriptions = &color_target, + .num_color_targets = 1, + .depth_stencil_format = renderer.depthFormat(), + .has_depth_stencil_target = true, + }, + }, + "Frustum"); } struct alignas(16) ubo_t { @@ -45,12 +50,12 @@ namespace frustum_debug { CommandBuffer &cmdBuf) { SDL_GPUColorTargetInfo color_target; SDL_zero(color_target); - color_target.texture = renderer.swapchain; + color_target.texture = renderer.colorTarget(); color_target.load_op = SDL_GPU_LOADOP_LOAD; color_target.store_op = SDL_GPU_STOREOP_STORE; SDL_GPUDepthStencilTargetInfo depth_target; SDL_zero(depth_target); - depth_target.texture = renderer.depth_texture; + depth_target.texture = renderer.depthTarget(); depth_target.load_op = SDL_GPU_LOADOP_LOAD; depth_target.store_op = SDL_GPU_STOREOP_STORE; depth_target.stencil_load_op = SDL_GPU_LOADOP_DONT_CARE; @@ -107,7 +112,7 @@ FrustumBoundsDebugSystem::FrustumBoundsDebugSystem( entt::registry ®istry, const RenderContext &renderer) : renderer(renderer) , device(renderer.device) - , pipeline(nullptr) + , pipeline(NoInit) , _registry(registry) { pipeline = frustum_debug::createFrustumDebugPipeline(renderer); } @@ -118,7 +123,7 @@ void FrustumBoundsDebugSystem::render(CommandBuffer &cmdBuf, SDL_GPURenderPass *render_pass = frustum_debug::getDefaultRenderPass(renderer, cmdBuf); - SDL_BindGPUGraphicsPipeline(render_pass, pipeline); + pipeline.bind(render_pass); for (auto [ent, item] : reg.view().each()) { frustum_debug::renderFrustum(cmdBuf, render_pass, camera, *item.otherCam, diff --git a/src/candlewick/core/debug/Frustum.h b/src/candlewick/core/debug/Frustum.h index d2228a58..fe9cc4fc 100644 --- a/src/candlewick/core/debug/Frustum.h +++ b/src/candlewick/core/debug/Frustum.h @@ -1,8 +1,8 @@ #pragma once -#include "../Core.h" #include "../Device.h" #include "../Collision.h" +#include "../GraphicsPipeline.h" #include "../math_types.h" #include @@ -12,8 +12,7 @@ namespace candlewick { namespace frustum_debug { - SDL_GPUGraphicsPipeline * - createFrustumDebugPipeline(const RenderContext &renderer); + GraphicsPipeline createFrustumDebugPipeline(const RenderContext &renderer); void renderFrustum(CommandBuffer &cmdBuf, SDL_GPURenderPass *render_pass, const Camera &mainCamera, const Camera &otherCam, @@ -46,7 +45,7 @@ struct DebugBoundsComponent { class FrustumBoundsDebugSystem final { const RenderContext &renderer; const Device &device; - SDL_GPUGraphicsPipeline *pipeline; + GraphicsPipeline pipeline; entt::registry &_registry; public: @@ -74,12 +73,10 @@ class FrustumBoundsDebugSystem final { void render(CommandBuffer &cmdBuf, const Camera &camera); - void release() noexcept { - if (pipeline) - SDL_ReleaseGPUGraphicsPipeline(device, pipeline); - } + void release() noexcept { pipeline.release(); } - ~FrustumBoundsDebugSystem() { release(); } + void update() {} }; +static_assert(Scene); } // namespace candlewick diff --git a/src/candlewick/core/errors.h b/src/candlewick/core/errors.h index d148aadd..1df43286 100644 --- a/src/candlewick/core/errors.h +++ b/src/candlewick/core/errors.h @@ -46,20 +46,28 @@ namespace detail { } // namespace detail +// source_location default for last argument using ctad trick, see +// https://stackoverflow.com/a/71082768 +template struct terminate_with_message { + [[noreturn]] terminate_with_message( + std::string_view fmt, Ts &&...args, + std::source_location location = std::source_location::current()) { + throw std::runtime_error(detail::error_message_format( + location.function_name(), fmt, std::forward(args)...)); + } + + [[noreturn]] terminate_with_message(std::source_location location, + std::string_view fmt, Ts &&...args) + : terminate_with_message(fmt, std::forward(args)..., location) {} +}; + template -[[noreturn]] -void terminate_with_message(std::source_location location, std::string_view fmt, - Ts &&...args) { - throw std::runtime_error(detail::error_message_format( - location.function_name(), fmt, std::forward(args)...)); -} +terminate_with_message(std::string_view, Ts &&...) + -> terminate_with_message; template -[[noreturn]] -void terminate_with_message(std::string_view fmt, Ts &&...args) { - terminate_with_message(std::source_location::current(), fmt, - std::forward(args)...); -} +terminate_with_message(std::source_location, std::string_view, Ts &&...) + -> terminate_with_message; [[noreturn]] inline void unreachable_with_message( diff --git a/src/candlewick/core/file_dialog_gui.cpp b/src/candlewick/core/file_dialog_gui.cpp index 4b837210..d108d531 100644 --- a/src/candlewick/core/file_dialog_gui.cpp +++ b/src/candlewick/core/file_dialog_gui.cpp @@ -51,21 +51,6 @@ static const SDL_Folder g_dialog_file_type_folder[] = { SDL_FOLDER_VIDEOS, }; -void guiAddFileDialog(SDL_Window *window, DialogFileType dialog_file_type, - std::string &out) { - const char *initial_path = - SDL_GetUserFolder(g_dialog_file_type_folder[int(dialog_file_type)]); - - auto [filters, nfilters] = g_file_filters[int(dialog_file_type)]; - - if (ImGui::Button("Select...")) { - SDL_ShowSaveFileDialog(fileCallbackImpl, &out, window, filters, nfilters, - initial_path); - } - ImGui::SameLine(); - ImGui::Text("%s", out.empty() ? "(none)" : out.c_str()); -} - void generateMediaFilenameFromTimestamp(const char *prefix, std::string &out, const char *extension, DialogFileType file_type) { @@ -80,4 +65,21 @@ void generateMediaFilenameFromTimestamp(const char *prefix, std::string &out, out = view; } +namespace gui { + void addFileDialog(SDL_Window *window, DialogFileType dialog_file_type, + std::string &out) { + const char *initial_path = + SDL_GetUserFolder(g_dialog_file_type_folder[int(dialog_file_type)]); + + auto [filters, nfilters] = g_file_filters[int(dialog_file_type)]; + + if (ImGui::Button("Select...")) { + SDL_ShowSaveFileDialog(fileCallbackImpl, &out, window, filters, nfilters, + initial_path); + } + ImGui::SameLine(); + ImGui::Text("%s", out.empty() ? "(none)" : out.c_str()); + } +} // namespace gui + } // namespace candlewick diff --git a/src/candlewick/multibody/LoadCoalPrimitives.h b/src/candlewick/multibody/LoadCoalPrimitives.h deleted file mode 100644 index 597bfbfb..00000000 --- a/src/candlewick/multibody/LoadCoalPrimitives.h +++ /dev/null @@ -1,8 +0,0 @@ -#pragma once - -#include "candlewick/deprecated.h" -#include "candlewick/multibody/LoadCoalGeometries.h" - -CANDLEWICK_DEPRECATED_HEADER( - "This header is deprecated. Use " - " instead.") diff --git a/src/candlewick/multibody/LoadPinocchioGeometry.cpp b/src/candlewick/multibody/LoadPinocchioGeometry.cpp index aa7e4729..cb8757fc 100644 --- a/src/candlewick/multibody/LoadPinocchioGeometry.cpp +++ b/src/candlewick/multibody/LoadPinocchioGeometry.cpp @@ -1,5 +1,5 @@ #include "LoadPinocchioGeometry.h" -#include "LoadCoalGeometries.h" +#include "../core/LoadCoalGeometries.h" #include "../core/errors.h" #include "../utils/LoadMesh.h" diff --git a/src/candlewick/multibody/Multibody.h b/src/candlewick/multibody/Multibody.h index 4372b2d8..279afb95 100644 --- a/src/candlewick/multibody/Multibody.h +++ b/src/candlewick/multibody/Multibody.h @@ -20,12 +20,13 @@ namespace multibody { using Inertiaf = pin::InertiaTpl; using Forcef = pin::ForceTpl; - /// \ingroup gui_util - /// \brief Display Pinocchio model and geometry model info in ImGui. - /// \image html robot-info-panel.png "Robot information panel." - void guiAddPinocchioModelInfo(entt::registry ®, const pin::Model &model, - const pin::GeometryModel &geom_model, - int table_height_lines = 6); + namespace gui { + /// \brief Display Pinocchio model and geometry model info in ImGui. + /// \image html robot-info-panel.png "Robot information panel." + void addPinocchioModelInfo(entt::registry ®, const pin::Model &model, + const pin::GeometryModel &geom_model, + int table_height_lines = 6); + } // namespace gui struct PinGeomObjComponent { pin::GeomIndex geom_index; diff --git a/src/candlewick/multibody/RobotDebug.cpp b/src/candlewick/multibody/RobotDebug.cpp index 1db0c949..f33d6365 100644 --- a/src/candlewick/multibody/RobotDebug.cpp +++ b/src/candlewick/multibody/RobotDebug.cpp @@ -1,10 +1,14 @@ #include "RobotDebug.h" - #include "../core/Components.h" #include +#include + +#include namespace candlewick::multibody { +namespace core_gui = ::candlewick::gui; + entt::entity RobotDebugSystem::addFrameTriad(pin::FrameIndex frame_id, const Float3 &scale) { entt::registry ® = m_scene.registry(); @@ -116,4 +120,76 @@ void RobotDebugSystem::destroyEntities() { m_scene.registry()); } +Float4 boostLuminance(const Float4 &color, float factor) { + // Simple approximation - proper would use HSV conversion + Float3 rgb = color.head<3>(); + float maxComp = rgb.maxCoeff(); + if (maxComp > 0) { + rgb *= std::min(1.0f, factor * maxComp) / maxComp; + } + return Float4(rgb.x(), rgb.y(), rgb.z(), color.w()); +} + +void RobotDebugSystem::renderDebugGui(const char *title) { + auto ®istry = m_scene.registry(); + + if (ImGui::CollapsingHeader(title)) { + char label[64]; + ImGui::SeparatorText("Frame placements"); + auto view = registry.view(); + for (auto [ent, dmc, fc] : view.each()) { + auto frame_name = m_robotModel->frames[fc].name.c_str(); + SDL_snprintf(label, 64, "frame_%d", int(fc)); + ImGui::PushID(label); + core_gui::addDebugMesh(dmc); + ImGui::SameLine(); + ImGui::Text("%s", frame_name); + ImGui::PopID(); + } + + ImGui::SeparatorText("Frame vels."); + auto view2 = + registry.view(); + for (auto [ent, dmc, fvc] : view2.each()) { + auto frame_name = m_robotModel->frames[fvc].name.c_str(); + SDL_snprintf(label, 64, "frame_vel_%d", int(fvc)); + ImGui::PushID(label); + core_gui::addDebugMesh(dmc); + ImGui::SameLine(); + ImGui::Text("%s", frame_name); + ImGui::PopID(); + } + + ImGui::SeparatorText("External forces"); + auto view3 = + registry.view(); + for (auto [ent, dmc, efc] : view3.each()) { + pin::FrameIndex fid = efc.frame_id; + auto frame_name = m_robotModel->frames[fid].name.c_str(); + float magnitude = efc.force.linear().norm(); + + SDL_snprintf(label, 64, "frame_force_%zu", fid); + + if (ImGui::Selectable(label, false, ImGuiSelectableFlags_AllowOverlap)) { + } + bool hovered = ImGui::IsItemHovered(); + ImGui::SameLine(); + ImGui::Text("frame %s: %.2f", frame_name, magnitude); + + if (hovered) { + ImGui::BeginTooltip(); + ImGui::Text("Frame: %s (ID: %zu)", frame_name, fid); + ImGui::Text("Magnitude: %.3f [N]", magnitude); + ImGui::Text("Lifetime: %d frames", efc.lifetime); + ImGui::EndTooltip(); + float lum_factor = + 1.5f + 1.f * std::sin(0.00973f * float(SDL_GetTicks())); + dmc.colors[0] = boostLuminance(efc.orig_color, lum_factor); + } else { + dmc.colors[0] = efc.orig_color; + } + } + } +} + } // namespace candlewick::multibody diff --git a/src/candlewick/multibody/RobotDebug.h b/src/candlewick/multibody/RobotDebug.h index cfc04322..d5c53da4 100644 --- a/src/candlewick/multibody/RobotDebug.h +++ b/src/candlewick/multibody/RobotDebug.h @@ -12,6 +12,7 @@ struct ExternalForceComponent { pin::FrameIndex frame_id; //< Frame at which the force applies Forcef force{Forcef::Zero()}; //< Force value int lifetime = 1; //< Arrow lifetime + Float4 orig_color; }; /// \brief A debug system for use with Pinocchio models and geometries. @@ -41,6 +42,8 @@ struct RobotDebugSystem final : IDebugSubSystem { void destroyEntities(); + void renderDebugGui(const char *title); + ~RobotDebugSystem() { this->destroyEntities(); this->m_robotModel = nullptr; diff --git a/src/candlewick/multibody/RobotScene.cpp b/src/candlewick/multibody/RobotScene.cpp index 331a7dda..b125013c 100644 --- a/src/candlewick/multibody/RobotScene.cpp +++ b/src/candlewick/multibody/RobotScene.cpp @@ -11,6 +11,7 @@ #include #include +#include #include #include @@ -60,9 +61,9 @@ void updateRobotTransforms(entt::registry ®istry, auto RobotScene::pinGeomToPipeline(const coal::CollisionGeometry &geom) -> PipelineType { - using enum coal::OBJECT_TYPE; const auto objType = geom.getObjectType(); switch (objType) { + using enum coal::OBJECT_TYPE; case OT_GEOM: return PIPELINE_TRIANGLEMESH; case OT_HFIELD: @@ -126,7 +127,8 @@ RobotScene::RobotScene(entt::registry ®istry, const RenderContext &renderer) , m_config() , m_geomModel(nullptr) , m_geomData(nullptr) - , m_initialized(false) { + , m_initialized(false) + , m_pipelines() { assert(!hasInternalPointers()); SDL_zero(directionalLight); directionalLight[1].direction = {-1.f, 1.f, -1.f}; @@ -143,54 +145,82 @@ RobotScene::RobotScene(entt::registry ®istry, const RenderContext &renderer, this->loadModels(geom_model, geom_data); } +auto createTextureWithMultisampledVariant(const Device &device, + SDL_GPUTextureCreateInfo texture_desc, + const char *name) { + const auto sample_count = texture_desc.sample_count; + auto texture_desc_resolve = texture_desc; + texture_desc_resolve.sample_count = SDL_GPU_SAMPLECOUNT_1; + if (!SDL_GPUTextureSupportsSampleCount(device, texture_desc.format, + sample_count)) { + terminate_with_message( + "Texture with format {:s} does not support sample count {:d}", + magic_enum::enum_name(texture_desc.format), + sdlSampleToValue(sample_count)); + } + if (!SDL_GPUTextureSupportsFormat(device, texture_desc.format, + texture_desc.type, texture_desc.usage)) { + terminate_with_message("Texture format + type + usage unsupported."); + } + return std::tuple{ + Texture{device, texture_desc, name}, + Texture{device, texture_desc_resolve, name}, + }; +} + void RobotScene::initGBuffer() { - const auto [width, height] = m_renderer.window.size(); - gBuffer.normalMap = Texture{m_renderer.device, - { - .type = SDL_GPU_TEXTURETYPE_2D, - .format = SDL_GPU_TEXTUREFORMAT_R16G16_FLOAT, - .usage = SDL_GPU_TEXTUREUSAGE_COLOR_TARGET | - SDL_GPU_TEXTUREUSAGE_SAMPLER, - .width = Uint32(width), - .height = Uint32(height), - .layer_count_or_depth = 1, - .num_levels = 1, - .sample_count = SDL_GPU_SAMPLECOUNT_1, - .props = 0, - }, - "GBuffer normal"}; - - gBuffer.accumTexture = - Texture{device(), - { - .type = SDL_GPU_TEXTURETYPE_2D, - .format = SDL_GPU_TEXTUREFORMAT_R16G16B16A16_FLOAT, - .usage = SDL_GPU_TEXTUREUSAGE_COLOR_TARGET | - SDL_GPU_TEXTUREUSAGE_SAMPLER, - .width = Uint32(width), - .height = Uint32(height), - .layer_count_or_depth = 1, - .num_levels = 1, - .sample_count = SDL_GPU_SAMPLECOUNT_1, - .props = 0, - }, - "WBOIT Accumulation"}; - - gBuffer.revealTexture = - Texture{device(), - { - .type = SDL_GPU_TEXTURETYPE_2D, - .format = SDL_GPU_TEXTUREFORMAT_R8_UNORM, - .usage = SDL_GPU_TEXTUREUSAGE_COLOR_TARGET | - SDL_GPU_TEXTUREUSAGE_SAMPLER, - .width = Uint32(width), - .height = Uint32(height), - .layer_count_or_depth = 1, - .num_levels = 1, - .sample_count = SDL_GPU_SAMPLECOUNT_1, - .props = 0, - }, - "WBOIT Revelage"}; + auto sample_count = m_renderer.getMsaaSampleCount(); + const auto [width, height] = m_renderer.window.sizeInPixels(); + std::tie(gBuffer.normalMap, gBuffer.resolveNormalMap) = + createTextureWithMultisampledVariant( + device(), + { + .type = SDL_GPU_TEXTURETYPE_2D, + .format = SDL_GPU_TEXTUREFORMAT_R16G16_FLOAT, + .usage = SDL_GPU_TEXTUREUSAGE_COLOR_TARGET | + SDL_GPU_TEXTUREUSAGE_SAMPLER, + .width = Uint32(width), + .height = Uint32(height), + .layer_count_or_depth = 1, + .num_levels = 1, + .sample_count = sample_count, + .props = 0, + }, + "GBuffer [Normal map]"); + + std::tie(gBuffer.accumTexture, gBuffer.resolveAccumTexture) = + createTextureWithMultisampledVariant( + device(), + { + .type = SDL_GPU_TEXTURETYPE_2D, + .format = SDL_GPU_TEXTUREFORMAT_R16G16B16A16_FLOAT, + .usage = SDL_GPU_TEXTUREUSAGE_COLOR_TARGET | + SDL_GPU_TEXTUREUSAGE_SAMPLER, + .width = Uint32(width), + .height = Uint32(height), + .layer_count_or_depth = 1, + .num_levels = 1, + .sample_count = sample_count, + .props = 0, + }, + "WBOIT Accumulation"); + + std::tie(gBuffer.revealTexture, gBuffer.resolveRevealTexture) = + createTextureWithMultisampledVariant( + device(), + { + .type = SDL_GPU_TEXTURETYPE_2D, + .format = SDL_GPU_TEXTUREFORMAT_R8_UNORM, + .usage = SDL_GPU_TEXTUREUSAGE_COLOR_TARGET | + SDL_GPU_TEXTUREUSAGE_SAMPLER, + .width = Uint32(width), + .height = Uint32(height), + .layer_count_or_depth = 1, + .num_levels = 1, + .sample_count = sample_count, + .props = 0, + }, + "WBOIT Revealage"); SDL_GPUSamplerCreateInfo sic{ .min_filter = SDL_GPU_FILTER_LINEAR, @@ -228,6 +258,7 @@ void RobotScene::initCompositePipeline(const MeshLayout &layout) { .fill_mode = SDL_GPU_FILLMODE_FILL, .cull_mode = SDL_GPU_CULLMODE_NONE, }, + .multisample_state{m_renderer.getMsaaSampleCount()}, .depth_stencil_state{ .enable_depth_test = false, .enable_depth_write = false, @@ -239,43 +270,38 @@ void RobotScene::initCompositePipeline(const MeshLayout &layout) { }, }; - m_pipelines.wboitComposite = SDL_CreateGPUGraphicsPipeline(device(), &desc); - if (!m_pipelines.wboitComposite) { - terminate_with_message("Failed to create WBOIT pipeline: {:s}", - SDL_GetError()); - } + m_wboitComposite = GraphicsPipeline(device(), desc, "wboitComposite"); } void RobotScene::ensurePipelinesExist( - const std::set> - &required_pipelines) { + const std::set &required_pipelines) { if (!gBuffer.initialized()) { this->initGBuffer(); } const bool enable_shadows = m_config.enable_shadows; - for (auto &[layout, pipeline_type, transparent] : required_pipelines) { - // We only route wrt pipeline type and opacity status. - // If for some reason the set contains two entries for e.g. opaque triangle - // pipeline with different mesh layouts, then the first entry wins because - // it gets to create the pipeline. - if (!routePipeline(pipeline_type, transparent)) { - createRenderPipeline(layout, m_renderer.getSwapchainTextureFormat(), - m_renderer.depthFormat(), pipeline_type, - transparent); + for (auto &[layout, key] : required_pipelines) { + if (!m_pipelines.contains(key)) { + auto pipeline = createRenderPipeline( + key, layout, m_renderer.colorFormat(), m_renderer.depthFormat()); + m_pipelines.set(key, std::move(pipeline)); } - if (pipeline_type == PIPELINE_TRIANGLEMESH) { - if (!ssaoPass.pipeline) { - ssaoPass = ssao::SsaoPass(m_renderer, gBuffer.normalMap); + const bool has_msaa = m_renderer.msaaEnabled(); + // handle other pipelines for effects + if (key.type == PIPELINE_TRIANGLEMESH) { + if (!ssaoPass.pipeline.initialized()) { + ssaoPass = + ssao::SsaoPass(m_renderer, has_msaa ? gBuffer.resolveNormalMap + : gBuffer.normalMap); } // configure shadow pass if (enable_shadows && !shadowPass.initialized()) { shadowPass = ShadowMapPass(device(), layout, m_renderer.depthFormat(), m_config.shadow_config); } - if (!m_pipelines.wboitComposite) + if (!m_wboitComposite.initialized()) this->initCompositePipeline(layout); } } @@ -291,7 +317,7 @@ void RobotScene::loadModels(const pin::GeometryModel &geom_model, // Phase 1. Load robot geometries and collect parameters for creating the // required render pipelines. - std::set> required_pipelines; + std::set required_pipelines; for (pin::GeomIndex geom_id = 0; geom_id < geom_model.ngeoms; geom_id++) { @@ -315,7 +341,10 @@ void RobotScene::loadModels(const pin::GeometryModel &geom_model, addPipelineTagComponent(m_registry, entity, pipeline_type); auto &layout = mmc.mesh.layout(); - required_pipelines.insert({layout, pipeline_type, is_transparent}); + required_pipelines.insert( + {layout, {pipeline_type, is_transparent, RenderMode::FILL}}); + required_pipelines.insert( + {layout, {pipeline_type, is_transparent, RenderMode::LINE}}); } // Phase 2. Init our render pipelines. @@ -323,7 +352,7 @@ void RobotScene::loadModels(const pin::GeometryModel &geom_model, m_initialized = true; } -void RobotScene::updateTransforms() { +void RobotScene::update() { updateRobotTransforms(registry(), geomModel(), geomData()); } @@ -348,14 +377,12 @@ void RobotScene::renderOpaque(CommandBuffer &command_buffer, } renderPBRTriangleGeometry(command_buffer, camera, false); - renderOtherGeometry(command_buffer, camera); } void RobotScene::renderTransparent(CommandBuffer &command_buffer, const Camera &camera) { renderPBRTriangleGeometry(command_buffer, camera, true); - compositeTransparencyPass(command_buffer); } @@ -369,19 +396,20 @@ void RobotScene::render(CommandBuffer &command_buffer, const Camera &camera) { /// with just two configuration options: whether to load or clear the color and /// depth targets. static SDL_GPURenderPass * -getRenderPass(const RenderContext &renderer, CommandBuffer &command_buffer, - SDL_GPULoadOp color_load_op, SDL_GPULoadOp depth_load_op, - bool has_normals_target, const RobotScene::GBuffer &gbuffer) { +getOpaqueRenderPass(const RenderContext &renderer, + CommandBuffer &command_buffer, SDL_GPULoadOp color_load_op, + SDL_GPULoadOp depth_load_op, bool has_normals_target, + const RobotScene::GBuffer &gbuffer) { SDL_GPUColorTargetInfo color_targets[2]; SDL_zero(color_targets); - color_targets[0].texture = renderer.swapchain; + color_targets[0].texture = renderer.colorTarget(); color_targets[0].load_op = color_load_op; color_targets[0].store_op = SDL_GPU_STOREOP_STORE; color_targets[0].cycle = false; SDL_GPUDepthStencilTargetInfo depth_target; SDL_zero(depth_target); - depth_target.texture = renderer.depth_texture; + depth_target.texture = renderer.depthTarget(); depth_target.clear_depth = 1.0f; depth_target.load_op = depth_load_op; depth_target.store_op = SDL_GPU_STOREOP_STORE; @@ -392,6 +420,10 @@ getRenderPass(const RenderContext &renderer, CommandBuffer &command_buffer, color_targets[1].load_op = SDL_GPU_LOADOP_CLEAR; color_targets[1].store_op = SDL_GPU_STOREOP_STORE; color_targets[1].cycle = false; + if (renderer.msaaEnabled()) { + color_targets[1].resolve_texture = gbuffer.resolveNormalMap; + color_targets[1].store_op = SDL_GPU_STOREOP_RESOLVE; + } } Uint32 num_color_targets = has_normals_target ? 2 : 1; return SDL_BeginGPURenderPass(command_buffer, color_targets, @@ -415,7 +447,7 @@ getTransparentRenderPass(const RenderContext &renderer, SDL_GPUDepthStencilTargetInfo depth_target; SDL_zero(depth_target); - depth_target.texture = renderer.depth_texture; + depth_target.texture = renderer.depthTarget(); depth_target.load_op = SDL_GPU_LOADOP_LOAD; depth_target.store_op = SDL_GPU_STOREOP_STORE; @@ -424,19 +456,24 @@ getTransparentRenderPass(const RenderContext &renderer, void RobotScene::compositeTransparencyPass(CommandBuffer &command_buffer) { // transparent triangle pipeline required - if (!m_pipelines.triangleMeshTransparent || !m_pipelines.wboitComposite) + if (!m_wboitComposite.initialized()) return; SDL_GPUColorTargetInfo target; SDL_zero(target); - target.texture = m_renderer.swapchain; + target.texture = m_renderer.colorTarget(); // op is LOAD - we want to keep results from opaque pass target.load_op = SDL_GPU_LOADOP_LOAD; target.store_op = SDL_GPU_STOREOP_STORE; + const bool has_msaa = m_renderer.msaaEnabled(); + if (has_msaa) { + target.resolve_texture = m_renderer.resolvedColorTarget(); + target.store_op = SDL_GPU_STOREOP_RESOLVE; + } SDL_GPURenderPass *render_pass = SDL_BeginGPURenderPass(command_buffer, &target, 1, nullptr); - SDL_BindGPUGraphicsPipeline(render_pass, m_pipelines.wboitComposite); + m_wboitComposite.bind(render_pass); // Bind accumulation and revealage textures rend::bindFragmentSamplers( @@ -456,11 +493,6 @@ void RobotScene::renderPBRTriangleGeometry(CommandBuffer &command_buffer, const Camera &camera, bool transparent) { - auto *pipeline = routePipeline(PIPELINE_TRIANGLEMESH, transparent); - if (!pipeline) { - return; - } - const Uint32 numLights = shadowPass.numLights(); // calculate light ubos LightArrayUbo lightUbo; @@ -472,7 +504,6 @@ void RobotScene::renderPBRTriangleGeometry(CommandBuffer &command_buffer, lightUbo.intensity[i].x() = dl.intensity; } - const bool enable_shadows = m_config.enable_shadows; ShadowAtlasInfoUbo shadowAtlasUbo{ .regions{}, }; @@ -484,17 +515,19 @@ void RobotScene::renderPBRTriangleGeometry(CommandBuffer &command_buffer, } const Mat4f viewProj = camera.viewProj(); - // this is the first render pass, hence: - // clear the color texture (swapchain), either load or clear the depth texture - SDL_GPURenderPass *render_pass = - transparent - ? getTransparentRenderPass(m_renderer, command_buffer, gBuffer) - : getRenderPass(m_renderer, command_buffer, SDL_GPU_LOADOP_CLEAR, - m_config.triangle_has_prepass ? SDL_GPU_LOADOP_LOAD - : SDL_GPU_LOADOP_CLEAR, - m_config.enable_normal_target, gBuffer); - - if (enable_shadows) { + // if geometry is opaque, this is the first render pass, hence we clear the + // color target transparent objects do not participate in SSAO + SDL_GPURenderPass *render_pass; + if (transparent) { + render_pass = getTransparentRenderPass(m_renderer, command_buffer, gBuffer); + } else { + render_pass = getOpaqueRenderPass( + m_renderer, command_buffer, SDL_GPU_LOADOP_CLEAR, + pbrHasPrepass() ? SDL_GPU_LOADOP_LOAD : SDL_GPU_LOADOP_CLEAR, true, + gBuffer); + } + + if (shadowsEnabled()) { rend::bindFragmentSamplers(render_pass, SHADOW_MAP_SLOT, {{ .texture = shadowPass.shadowMap, @@ -512,24 +545,25 @@ void RobotScene::renderPBRTriangleGeometry(CommandBuffer &command_buffer, .pushFragmentUniform(FragmentUniformSlots::SSAO_FLAG, _useSsao) .pushFragmentUniform(FragmentUniformSlots::ATLAS_INFO, shadowAtlasUbo); - SDL_BindGPUGraphicsPipeline(render_pass, pipeline); - auto view = m_registry.view>( entt::exclude); - auto process_entities = [&](const TransformComponent &tr, const auto &obj) { + auto process_entities = [&](entt::entity ent) { + auto [tr, obj] = + m_registry.get( + ent); const Mat4f modelView = camera.view * tr; const Mesh &mesh = obj.mesh; - Mat4f mvp = viewProj * tr; + const Mat4f mvp = viewProj * tr; TransformUniformData data{ .modelView = modelView, .mvp = mvp, .normalMatrix = math::computeNormalMatrix(modelView), }; command_buffer.pushVertexUniform(VertexUniformSlots::TRANSFORM, data); - if (enable_shadows) { + if (shadowsEnabled()) { LightSpaceMatricesUbo shadowUbo; shadowUbo.numLights = numLights; for (size_t i = 0; i < numLights; i++) { @@ -548,10 +582,38 @@ void RobotScene::renderPBRTriangleGeometry(CommandBuffer &command_buffer, }; if (transparent) { - (view | m_registry.view(entt::exclude)) - .each(process_entities); + auto *pipeline = + m_pipelines.get({PIPELINE_TRIANGLEMESH, true, RenderMode::FILL}); + if (!pipeline) + return; + pipeline->bind(render_pass); + for (entt::entity ent : + view | m_registry.view(entt::exclude)) { + process_entities(ent); + } } else { - (view | m_registry.view()).each(process_entities); + auto opaques = view | m_registry.view(); + auto get_filter = [® = this->m_registry](RenderMode ref_mode) { + return std::views::filter([®, ref_mode](entt::entity ent) { + return reg.get(ent).mode == ref_mode; + }); + }; + + if (auto pipeline = + m_pipelines.get({PIPELINE_TRIANGLEMESH, false, RenderMode::FILL})) { + pipeline->bind(render_pass); + for (auto ent : opaques | get_filter(RenderMode::FILL)) { + process_entities(ent); + } + } + + if (auto pipeline = + m_pipelines.get({PIPELINE_TRIANGLEMESH, false, RenderMode::LINE})) { + pipeline->bind(render_pass); + for (auto ent : opaques | get_filter(RenderMode::LINE)) { + process_entities(ent); + } + } } SDL_EndGPURenderPass(render_pass); @@ -560,8 +622,8 @@ void RobotScene::renderPBRTriangleGeometry(CommandBuffer &command_buffer, void RobotScene::renderOtherGeometry(CommandBuffer &command_buffer, const Camera &camera) { SDL_GPURenderPass *render_pass = - getRenderPass(m_renderer, command_buffer, SDL_GPU_LOADOP_LOAD, - SDL_GPU_LOADOP_LOAD, false, gBuffer); + getOpaqueRenderPass(m_renderer, command_buffer, SDL_GPU_LOADOP_LOAD, + SDL_GPU_LOADOP_LOAD, false, gBuffer); const Mat4f viewProj = camera.viewProj(); @@ -570,10 +632,11 @@ void RobotScene::renderOtherGeometry(CommandBuffer &command_buffer, if (current_pipeline_type == PIPELINE_TRIANGLEMESH) return; - auto *pipeline = routePipeline(current_pipeline_type); + auto *pipeline = + m_pipelines.get({current_pipeline_type, false, RenderMode::FILL}); if (!pipeline) return; - SDL_BindGPUGraphicsPipeline(render_pass, pipeline); + pipeline->bind(render_pass); auto env_view = m_registry.view transparency: %s", transparent ? "true" : "false"); + SDL_Log(" > render mode: %s", magic_enum::enum_name(renderMode).data()); SDL_Log(" > depth compare op: %s", magic_enum::enum_name(depth_compare_op).data()); SDL_Log(" > prepass: %s", had_prepass ? "true" : "false"); } - desc.rasterizer_state.cull_mode = pipe_config.cull_mode; - desc.rasterizer_state.fill_mode = pipe_config.fill_mode; - routePipeline(type, transparent) = - SDL_CreateGPUGraphicsPipeline(device(), &desc); + + return GraphicsPipeline(device(), desc, nullptr); } } // namespace candlewick::multibody diff --git a/src/candlewick/multibody/RobotScene.h b/src/candlewick/multibody/RobotScene.h index 2aac5e18..41cd24ae 100644 --- a/src/candlewick/multibody/RobotScene.h +++ b/src/candlewick/multibody/RobotScene.h @@ -6,7 +6,6 @@ #include "Multibody.h" #include "../core/Device.h" -#include "../core/Scene.h" #include "../core/RenderContext.h" #include "../core/LightUniforms.h" #include "../core/DepthAndShadowPass.h" @@ -21,6 +20,7 @@ #include namespace candlewick { +enum class RenderMode; /// \brief Terminate the application after encountering an invalid enum value. template @@ -66,13 +66,12 @@ namespace multibody { void initCompositePipeline(const MeshLayout &layout); public: - enum PipelineType { + enum class PipelineType { PIPELINE_TRIANGLEMESH, PIPELINE_HEIGHTFIELD, PIPELINE_POINTCLOUD, }; - static constexpr size_t kNumPipelineTypes = - magic_enum::enum_count(); + using enum PipelineType; enum VertexUniformSlots : Uint32 { TRANSFORM, LIGHT_MATRICES }; enum FragmentUniformSlots : Uint32 { MATERIAL, @@ -98,14 +97,13 @@ namespace multibody { } } - template using pipeline_tag = entt::tag; + template using pipeline_tag = entt::integral_constant; struct PipelineConfig { // shader set const char *vertex_shader_path; const char *fragment_shader_path; SDL_GPUCullMode cull_mode = SDL_GPU_CULLMODE_BACK; - SDL_GPUFillMode fill_mode = SDL_GPU_FILLMODE_FILL; }; struct Config { struct TrianglePipelineConfig { @@ -124,29 +122,80 @@ namespace multibody { .fragment_shader_path = "Hud3dElement.frag", }; PipelineConfig pointcloud_config; - bool enable_msaa = false; bool enable_shadows = true; bool enable_ssao = true; bool triangle_has_prepass = false; - bool enable_normal_target = false; - SDL_GPUSampleCount msaa_samples = SDL_GPU_SAMPLECOUNT_1; ShadowPassConfig shadow_config; }; + struct PipelineKey { + PipelineType type; + bool transparent; + RenderMode renderMode; + + auto operator<=>(const PipelineKey &) const = default; + }; + class PipelineManager { + std::map m_store; + + public: + PipelineManager() : m_store() {} + + bool contains(const PipelineKey &key) const { + return m_store.contains(key); + } + + PipelineManager(const PipelineManager &) = delete; + PipelineManager(PipelineManager &&) = default; + PipelineManager &operator=(const PipelineManager &) = delete; + + GraphicsPipeline *get(const PipelineKey &key) { + auto it = m_store.find(key); + if (it != m_store.cend()) + return &it->second; + + return nullptr; + } + + void set(const PipelineKey &key, GraphicsPipeline &&pipeline) { + m_store.emplace(key, std::move(pipeline)); + } + + void clear() { m_store.clear(); } + }; + std::array directionalLight; ssao::SsaoPass ssaoPass{NoInit}; struct GBuffer { Texture normalMap{NoInit}; + Texture resolveNormalMap{NoInit}; // WBOIT buffers Texture accumTexture{NoInit}; Texture revealTexture{NoInit}; + Texture resolveAccumTexture{NoInit}; + Texture resolveRevealTexture{NoInit}; SDL_GPUSampler *sampler = nullptr; // composite pass bool initialized() const { return (sampler != nullptr) && normalMap && accumTexture && revealTexture; } + + void release() noexcept { + auto *device = normalMap.device(); + normalMap.destroy(); + resolveNormalMap.destroy(); + accumTexture.destroy(); + revealTexture.destroy(); + resolveAccumTexture.destroy(); + resolveRevealTexture.destroy(); + if (sampler) { + SDL_ReleaseGPUSampler(device, sampler); + sampler = nullptr; + } + } + ~GBuffer() noexcept { this->release(); } } gBuffer; ShadowMapPass shadowPass{NoInit}; @@ -163,9 +212,10 @@ namespace multibody { RobotScene(const RobotScene &) = delete; void setConfig(const Config &config) { - CANDLEWICK_ASSERT( - !m_initialized, - "Cannot call setConfig() after render system was initialized."); + if (m_initialized) + terminate_with_message( + "Cannot call setConfig() after render system was initialized."); + m_config = config; } @@ -175,7 +225,7 @@ namespace multibody { const pin::GeometryData &geom_data); /// \brief Update the transform component of the GeometryObject entities. - void updateTransforms(); + void update(); void collectOpaqueCastables(); const std::vector &castables() const { return m_castables; } @@ -198,10 +248,10 @@ namespace multibody { /// (Pinocchio geometry objects). void clearRobotGeometries(); - void createRenderPipeline(const MeshLayout &layout, - SDL_GPUTextureFormat render_target_format, - SDL_GPUTextureFormat depth_stencil_format, - PipelineType type, bool transparent); + GraphicsPipeline + createRenderPipeline(const PipelineKey &key, const MeshLayout &layout, + SDL_GPUTextureFormat render_target_format, + SDL_GPUTextureFormat depth_stencil_format); /// \warning Call updateTransforms() before rendering the objects with /// this function. @@ -219,11 +269,11 @@ namespace multibody { inline bool pbrHasPrepass() const { return m_config.triangle_has_prepass; } inline bool shadowsEnabled() const { return m_config.enable_shadows; } + using pipeline_req_t = std::tuple; /// \brief Ensure the render pipelines were properly created following the /// provided requirements. - void ensurePipelinesExist( - const std::set> - &required_pipelines); + void + ensurePipelinesExist(const std::set &required_pipelines); /// \brief Getter for the pinocchio GeometryModel object. const pin::GeometryModel &geomModel() const { return *m_geomModel; } @@ -243,26 +293,8 @@ namespace multibody { const pin::GeometryData *m_geomData; std::vector m_castables; bool m_initialized; - struct { - SDL_GPUGraphicsPipeline *triangleMeshOpaque = nullptr; - SDL_GPUGraphicsPipeline *triangleMeshTransparent = nullptr; - SDL_GPUGraphicsPipeline *heightfield = nullptr; - SDL_GPUGraphicsPipeline *pointcloud = nullptr; - SDL_GPUGraphicsPipeline *wboitComposite = nullptr; - } m_pipelines; - - SDL_GPUGraphicsPipeline *&routePipeline(PipelineType type, - bool transparent = false) { - switch (type) { - case PIPELINE_TRIANGLEMESH: - return transparent ? m_pipelines.triangleMeshTransparent - : m_pipelines.triangleMeshOpaque; - case PIPELINE_HEIGHTFIELD: - return m_pipelines.heightfield; - case PIPELINE_POINTCLOUD: - return m_pipelines.pointcloud; - } - } + PipelineManager m_pipelines; + GraphicsPipeline m_wboitComposite{NoInit}; }; static_assert(Scene); diff --git a/src/candlewick/multibody/Visualizer.cpp b/src/candlewick/multibody/Visualizer.cpp index f308849d..047194d0 100644 --- a/src/candlewick/multibody/Visualizer.cpp +++ b/src/candlewick/multibody/Visualizer.cpp @@ -6,8 +6,11 @@ #include #include +#include namespace candlewick { +using namespace entt::literals; + const char *sdlMouseButtonToString(Uint8 button) { switch (button) { case SDL_BUTTON_LEFT: @@ -28,16 +31,19 @@ const char *sdlMouseButtonToString(Uint8 button) { namespace candlewick::multibody { -static RenderContext _create_renderer(const Visualizer::Config &config) { +static RenderContext _create_renderer(const Visualizer::Config &config, + SDL_WindowFlags flags = 0) { if (!SDL_Init(SDL_INIT_VIDEO)) { terminate_with_message("Failed to init video: {:s}", SDL_GetError()); } SDL_Log("Video driver: %s", SDL_GetCurrentVideoDriver()); - return RenderContext{Device{auto_detect_shader_format_subset()}, - Window{"Candlewick Pinocchio visualizer", - int(config.width), int(config.height), 0}, - config.depth_stencil_format}; + RenderContext r{Device{auto_detect_shader_format_subset()}, + Window{"Candlewick Pinocchio visualizer", int(config.width), + int(config.height), flags}, + config.depthStencilFormat}; + r.enableMSAA(config.sampleCount); + return r; } Visualizer::Visualizer(const Config &config, const pin::Model &model, @@ -45,7 +51,7 @@ Visualizer::Visualizer(const Config &config, const pin::Model &model, GuiSystem::GuiBehavior gui_callback) : BaseVisualizer{model, visual_model} , registry{} - , renderer{_create_renderer(config)} + , renderer{_create_renderer(config, SDL_WINDOW_HIGH_PIXEL_DENSITY)} , guiSystem{renderer, std::move(gui_callback)} , robotScene{registry, renderer} , debugScene{registry, renderer} @@ -78,7 +84,6 @@ Visualizer::Visualizer(const Config &config, const pin::Model &model, void Visualizer::initialize() { RobotScene::Config rconfig; rconfig.enable_shadows = true; - rconfig.enable_normal_target = true; robotScene.setConfig(rconfig); robotScene.directionalLight = { @@ -126,11 +131,11 @@ void Visualizer::resetCamera() { void Visualizer::loadViewerModel() { robotScene.loadModels(visualModel(), visualData()); - if (m_robotDebug) { - m_robotDebug->reload(this->model(), this->data()); + if (auto *robotDebug = debugScene.getSystem("robot"_hs)) { + robotDebug->reload(this->model(), this->data()); } else { - m_robotDebug = - &debugScene.addSystem(this->model(), this->data()); + debugScene.addSystem("robot"_hs, this->model(), + this->data()); std::tie(m_grid, std::ignore) = debugScene.addLineGrid(); } } @@ -168,8 +173,8 @@ void Visualizer::displayImpl() { this->processEvents(); + robotScene.update(); debugScene.update(); - robotScene.updateTransforms(); this->render(); if (m_shouldScreenshot) { @@ -181,9 +186,9 @@ void Visualizer::displayImpl() { #ifdef CANDLEWICK_WITH_FFMPEG_SUPPORT if (m_videoRecorder.isRecording()) { CommandBuffer command_buffer{device()}; - m_videoRecorder.writeTextureToVideoFrame( - command_buffer, device(), m_transferBuffers, renderer.swapchain, - renderer.getSwapchainTextureFormat()); + m_videoRecorder.writeTextureToFrame(command_buffer, device(), + m_transferBuffers, + renderer.resolvedColorTarget()); } #endif } @@ -191,21 +196,24 @@ void Visualizer::displayImpl() { void Visualizer::render() { CommandBuffer command_buffer = renderer.acquireCommandBuffer(); + robotScene.collectOpaqueCastables(); + std::span castables = robotScene.castables(); + renderShadowPassFromAABB(command_buffer, robotScene.shadowPass, + robotScene.directionalLight, castables, + worldSceneBounds); + + robotScene.renderOpaque(command_buffer, controller); + debugScene.render(command_buffer, controller); + robotScene.renderTransparent(command_buffer, controller); + if (m_showGui) + guiSystem.render(command_buffer); + if (renderer.waitAndAcquireSwapchain(command_buffer)) { - robotScene.collectOpaqueCastables(); - std::span castables = robotScene.castables(); - renderShadowPassFromAABB(command_buffer, robotScene.shadowPass, - robotScene.directionalLight, castables, - worldSceneBounds); - - auto &camera = controller.camera; - robotScene.renderOpaque(command_buffer, camera); - debugScene.render(command_buffer, camera); - robotScene.renderTransparent(command_buffer, camera); - if (m_showGui) - guiSystem.render(command_buffer); + // present (blit) main color target to swapchain + renderer.presentToSwapchain(command_buffer); + } else { + terminate_with_message("Failed to acquire swapchain: {:s}", SDL_GetError()); } - command_buffer.submit(); } @@ -214,8 +222,8 @@ void Visualizer::takeScreenshot(std::string_view filename) { auto [width, height] = renderer.window.sizeInPixels(); SDL_Log("Saving %dx%d screenshot at: '%s'", width, height, filename.data()); media::saveTextureToFile(command_buffer, device(), m_transferBuffers, - renderer.swapchain, - renderer.getSwapchainTextureFormat(), Uint16(width), + renderer.resolvedColorTarget(), + renderer.colorFormat(), Uint16(width), Uint16(height), filename); } @@ -264,12 +272,12 @@ auto cast_eigen_optional(const std::optional &xopt) { void Visualizer::addFrameViz(pin::FrameIndex id, bool show_velocity, std::optional scale_, std::optional vel_scale) { - assert(m_robotDebug); auto scale = cast_eigen_optional(scale_).value_or( RobotDebugSystem::DEFAULT_TRIAD_SCALE); - m_robotDebug->addFrameTriad(id, scale); + auto &robotDebug = debugScene.tryGetSystem("robot"_hs); + robotDebug.addFrameTriad(id, scale); if (show_velocity) - m_robotDebug->addFrameVelocityArrow( + robotDebug.addFrameVelocityArrow( id, vel_scale.value_or(RobotDebugSystem::DEFAULT_VEL_SCALE)); } @@ -291,14 +299,14 @@ void Visualizer::setFrameExternalForce(pin::FrameIndex frame_id, SDL_Log("Force arrow not found for frame %zu, adding arrow with lifetime %u", frame_id, initial_lifetime); #endif - auto ent = debugScene.addArrow(); + auto [ent, dmc] = debugScene.addArrow(); registry.emplace(ent, frame_id, force, - initial_lifetime); + initial_lifetime, dmc.colors[0]); } void Visualizer::removeFramesViz() { - assert(m_robotDebug); - m_robotDebug->destroyEntities(); + if (auto *p = debugScene.getSystem("robot"_hs)) + p->destroyEntities(); } } // namespace candlewick::multibody diff --git a/src/candlewick/multibody/Visualizer.h b/src/candlewick/multibody/Visualizer.h index 71051f10..19385728 100644 --- a/src/candlewick/multibody/Visualizer.h +++ b/src/candlewick/multibody/Visualizer.h @@ -52,11 +52,16 @@ void guiAddCameraParams(CylindricalCamera &controller, /// /// This visualizer is synchronous. The window is only updated when `display()` /// is called. +/// +/// \note So far, this visualizer class does not support displaying visual and +/// collision geometries simulatenously. +/// +/// \todo Add support for displaying visual *and* collision geometries at the +/// same time. class Visualizer final : public BaseVisualizer { bool m_showGui = true; bool m_shouldExit = false; entt::entity m_grid; - RobotDebugSystem *m_robotDebug = nullptr; void initialize(); @@ -86,7 +91,8 @@ class Visualizer final : public BaseVisualizer { struct Config { Uint32 width; Uint32 height; - SDL_GPUTextureFormat depth_stencil_format = SDL_GPU_TEXTUREFORMAT_D16_UNORM; + SDL_GPUSampleCount sampleCount = SDL_GPU_SAMPLECOUNT_2; + SDL_GPUTextureFormat depthStencilFormat = SDL_GPU_TEXTUREFORMAT_D16_UNORM; }; void resetCamera(); diff --git a/src/candlewick/multibody/pinocchio_info_gui.cpp b/src/candlewick/multibody/pinocchio_info_gui.cpp index 17267051..549e118b 100644 --- a/src/candlewick/multibody/pinocchio_info_gui.cpp +++ b/src/candlewick/multibody/pinocchio_info_gui.cpp @@ -10,11 +10,11 @@ #include #include -namespace candlewick::multibody { +namespace candlewick::multibody::gui { -void guiAddPinocchioModelInfo(entt::registry ®, const pin::Model &model, - const pin::GeometryModel &geom_model, - int table_height_lines) { +void addPinocchioModelInfo(entt::registry ®, const pin::Model &model, + const pin::GeometryModel &geom_model, + int table_height_lines) { ImGuiTableFlags flags = 0; flags |= ImGuiTableFlags_SizingStretchProp; flags |= ImGuiTableFlags_RowBg; @@ -69,7 +69,7 @@ void guiAddPinocchioModelInfo(entt::registry ®, const pin::Model &model, ImGui::Spacing(); ImGui::Text("No. of geometries: %zu", geom_model.ngeoms); - if (ImGui::BeginTable("pin_geom_table", 5, flags | ImGuiTableFlags_Sortable, + if (ImGui::BeginTable("pin_geom_table", 6, flags | ImGuiTableFlags_Sortable, outer_size)) { ImGui::TableSetupColumn("Index", ImGuiTableColumnFlags_DefaultSort, 0.0f, 0); @@ -77,6 +77,7 @@ void guiAddPinocchioModelInfo(entt::registry ®, const pin::Model &model, ImGui::TableSetupColumn("Object / node type", ImGuiTableColumnFlags_NoSort); ImGui::TableSetupColumn("Parent joint", ImGuiTableColumnFlags_NoSort); ImGui::TableSetupColumn("Show", ImGuiTableColumnFlags_NoSort); + ImGui::TableSetupColumn("Mode", ImGuiTableColumnFlags_NoSort); ImGui::TableHeadersRow(); if (ImGuiTableSortSpecs *sortSpecs = ImGui::TableGetSortSpecs()) { @@ -114,6 +115,9 @@ void guiAddPinocchioModelInfo(entt::registry ®, const pin::Model &model, for (auto [ent, id] : view.each()) { auto &gobj = geom_model.geometryObjects[id]; + + auto &mmc = reg.get(ent); + const coal::CollisionGeometry &coll = *gobj.geometry; coal::OBJECT_TYPE objType = coll.getObjectType(); coal::NODE_TYPE nodeType = coll.getNodeType(); @@ -131,13 +135,20 @@ void guiAddPinocchioModelInfo(entt::registry ®, const pin::Model &model, ImGui::Text("%zu (%s)", parent_joint, parent_joint_name); ImGui::TableNextColumn(); - char chk_label[32]; + char label[32]; bool enabled = !disabled.contains(ent); - SDL_snprintf(chk_label, 32, "###enabled%zu", pin::FrameIndex(id)); - guiAddDisableCheckbox(chk_label, reg, ent, enabled); + SDL_snprintf(label, 32, "gobj_%zu", pin::GeomIndex(id)); + ImGui::PushID(label); + + ::candlewick::gui::addDisableCheckbox("###enabled", reg, ent, enabled); + ImGui::TableNextColumn(); + const char *names[] = {"FILL", "LINE"}; + ImGui::Combo("###mode", (int *)&mmc.mode, names, IM_ARRAYSIZE(names)); + + ImGui::PopID(); } ImGui::EndTable(); } } -} // namespace candlewick::multibody +} // namespace candlewick::multibody::gui diff --git a/src/candlewick/multibody/visualizer_gui.cpp b/src/candlewick/multibody/visualizer_gui.cpp index 477f0bca..38394fae 100644 --- a/src/candlewick/multibody/visualizer_gui.cpp +++ b/src/candlewick/multibody/visualizer_gui.cpp @@ -1,14 +1,18 @@ #include "Visualizer.h" - -#include +#include "RobotDebug.h" #include #include + #include #include #include +#include + namespace candlewick::multibody { +namespace core_gui = ::candlewick::gui; +using namespace entt::literals; void guiAddCameraParams(CylindricalCamera &controller, CameraControlParams ¶ms) { @@ -29,30 +33,6 @@ void guiAddCameraParams(CylindricalCamera &controller, } } -void guiAddDebugMesh(DebugMeshComponent &dmc, - bool enable_pipeline_switch = true) { - ImGui::Checkbox("##enabled", &dmc.enable); - Uint32 col_id = 0; - ImGuiColorEditFlags color_flags = ImGuiColorEditFlags_NoAlpha | - ImGuiColorEditFlags_NoSidePreview | - ImGuiColorEditFlags_NoInputs; - char label[32]; - for (auto &col : dmc.colors) { - SDL_snprintf(label, sizeof(label), "##color##%u", col_id); - ImGui::SameLine(); - ImGui::ColorEdit4(label, col.data(), color_flags); - col_id++; - } - if (enable_pipeline_switch) { - const char *names[] = {"FILL", "LINE"}; - static_assert(IM_ARRAYSIZE(names) == - magic_enum::enum_count()); - ImGui::SameLine(); - ImGui::Combo("Mode##pipeline", (int *)&dmc.pipeline_type, names, - IM_ARRAYSIZE(names)); - } -} - void Visualizer::guiCallbackImpl() { // Verify ABI compatibility between caller code and compiled version of Dear @@ -63,7 +43,7 @@ void Visualizer::guiCallbackImpl() { if (show_imgui_about) ImGui::ShowAboutWindow(&show_imgui_about); if (show_our_about) - ::candlewick::showCandlewickAboutWindow(&show_our_about); + ::candlewick::gui::showCandlewickAboutWindow(&show_our_about); ImGuiWindowFlags window_flags = 0; window_flags |= ImGuiWindowFlags_AlwaysAutoResize; @@ -78,12 +58,13 @@ void Visualizer::guiCallbackImpl() { } ImGui::Text("Video driver: %s", SDL_GetCurrentVideoDriver()); - ImGui::Text("Display pixel density: %.2f / scale: %.2f", + ImGui::Text("Window pixel density: %.2f / display scale: %.2f", renderer.window.pixelDensity(), renderer.window.displayScale()); ImGui::Text("Device driver: %s", renderer.device.driverName()); if (ImGui::CollapsingHeader("Lights and camera controls")) { - guiAddLightControls(robotScene.directionalLight, robotScene.numLights()); + core_gui::addLightControls(robotScene.directionalLight, + robotScene.numLights()); guiAddCameraParams(controller, cameraParams); } @@ -94,7 +75,7 @@ void Visualizer::guiCallbackImpl() { ImGui::Text("%s", name); auto &dmc = registry.get(m_grid); ImGui::SameLine(); - guiAddDebugMesh(dmc, false); + core_gui::addDebugMesh(dmc, false); ImGui::PopID(); } ImGui::Checkbox("Ambient occlusion (SSAO)", @@ -103,7 +84,11 @@ void Visualizer::guiCallbackImpl() { if (ImGui::CollapsingHeader("Robot model info", ImGuiTreeNodeFlags_DefaultOpen)) { - guiAddPinocchioModelInfo(registry, m_model, visualModel()); + gui::addPinocchioModelInfo(registry, m_model, visualModel()); + } + + if (auto robotDebug = debugScene.getSystem("robot"_hs)) { + robotDebug->renderDebugGui("Robot debug"); } if (ImGui::CollapsingHeader( @@ -115,8 +100,8 @@ void Visualizer::guiCallbackImpl() { )) { ImGui::BeginChild("screenshot_taker", {0, 0}, ImGuiChildFlags_Borders | ImGuiChildFlags_AutoResizeY); - guiAddFileDialog(renderer.window, DialogFileType::IMAGES, - m_currentScreenshotFilename); + core_gui::addFileDialog(renderer.window, DialogFileType::IMAGES, + m_currentScreenshotFilename); if (ImGui::Button("Take screenshot")) { m_shouldScreenshot = true; if (m_currentScreenshotFilename.empty()) { @@ -130,8 +115,8 @@ void Visualizer::guiCallbackImpl() { #ifdef CANDLEWICK_WITH_FFMPEG_SUPPORT ImGui::BeginChild("video_record", {0, 0}, ImGuiChildFlags_Borders | ImGuiChildFlags_AutoResizeY); - guiAddFileDialog(renderer.window, DialogFileType::VIDEOS, - m_currentVideoFilename); + core_gui::addFileDialog(renderer.window, DialogFileType::VIDEOS, + m_currentVideoFilename); ImGui::BeginDisabled(m_videoRecorder.isRecording()); ImGui::SliderInt("bitrate", &m_videoSettings.bitRate, 2'000'000, 6'000'000); @@ -160,20 +145,6 @@ void Visualizer::guiCallbackImpl() { #endif }; - if (ImGui::CollapsingHeader("Robot debug")) { - auto view = registry.view(); - for (auto [ent, dmc, fc] : view.each()) { - auto frame_name = model().frames[fc].name.c_str(); - char label[64]; - SDL_snprintf(label, 64, "frame_%d", int(fc)); - ImGui::PushID(label); - guiAddDebugMesh(dmc); - ImGui::SameLine(); - ImGui::Text("%s", frame_name); - ImGui::PopID(); - } - } - ImGui::End(); } diff --git a/src/candlewick/posteffects/SSAO.cpp b/src/candlewick/posteffects/SSAO.cpp index da417bb3..a97bec60 100644 --- a/src/candlewick/posteffects/SSAO.cpp +++ b/src/candlewick/posteffects/SSAO.cpp @@ -124,35 +124,37 @@ namespace ssao { , inDepthMap(other.inDepthMap) , inNormalMap(other.inNormalMap) , texSampler(other.texSampler) - , pipeline(other.pipeline) + , pipeline(std::move(other.pipeline)) , ssaoMap(std::move(other.ssaoMap)) , ssaoNoise(std::move(other.ssaoNoise)) - , blurPipeline(other.blurPipeline) + , blurPipeline(std::move(other.blurPipeline)) , blurPass1Tex(std::move(other.blurPass1Tex)) { other._device = nullptr; } SsaoPass &SsaoPass::operator=(SsaoPass &&other) noexcept { - this->release(); + if (this != &other) { + this->release(); #define _c(name) name = std::move(other.name) - _c(_device); - _c(inDepthMap); - _c(inNormalMap); - _c(texSampler); - _c(pipeline); - _c(ssaoMap); - _c(ssaoNoise); - _c(blurPipeline); - _c(blurPass1Tex); + _c(_device); + _c(inDepthMap); + _c(inNormalMap); + _c(texSampler); + _c(pipeline); + _c(ssaoMap); + _c(ssaoNoise); + _c(blurPipeline); + _c(blurPass1Tex); #undef _c - other._device = nullptr; + other._device = nullptr; + } return *this; } SsaoPass::SsaoPass(const RenderContext &renderer, SDL_GPUTexture *normalMap) : _device(renderer.device) - , inDepthMap(renderer.depth_texture) + , inDepthMap(renderer.depthTarget()) , inNormalMap(normalMap) { const auto &device = renderer.device; @@ -198,11 +200,12 @@ namespace ssao { .num_color_targets = 1, .has_depth_stencil_target = false}, }; - pipeline = SDL_CreateGPUGraphicsPipeline(device, &pipeline_desc); + pipeline = GraphicsPipeline(device, pipeline_desc, "SSAO pipeline"); auto blurShader = Shader::fromMetadata(device, "SSAOblur.frag"); SDL_GPUGraphicsPipelineCreateInfo blur_pipeline_desc = pipeline_desc; blur_pipeline_desc.fragment_shader = blurShader; - blurPipeline = SDL_CreateGPUGraphicsPipeline(device, &blur_pipeline_desc); + blurPipeline = + GraphicsPipeline(device, blur_pipeline_desc, "SSAO pipeline [blur]"); // Now, we create the noise texture Uint32 num_pixels_rows = 4u; @@ -234,7 +237,7 @@ namespace ssao { cmdBuf .pushFragmentUniformRaw(0, KERNEL_SAMPLES.data(), SAMPLES_PAYLOAD_BYTES) .pushFragmentUniform(1, proj); - SDL_BindGPUGraphicsPipeline(render_pass, pipeline); + pipeline.bind(render_pass); SDL_DrawGPUPrimitives(render_pass, 6, 1, 0, 0); SDL_EndGPURenderPass(render_pass); @@ -245,7 +248,7 @@ namespace ssao { color_info.texture = (i == 0) ? blurPass1Tex : ssaoMap; render_pass = SDL_BeginGPURenderPass(cmdBuf, &color_info, 1, nullptr); - SDL_BindGPUGraphicsPipeline(render_pass, blurPipeline); + blurPipeline.bind(render_pass); cmdBuf.pushFragmentUniform(0, blurDir); rend::bindFragmentSamplers( @@ -264,17 +267,13 @@ namespace ssao { if (texSampler) SDL_ReleaseGPUSampler(_device, texSampler); texSampler = nullptr; - if (pipeline) - SDL_ReleaseGPUGraphicsPipeline(_device, pipeline); - pipeline = nullptr; if (ssaoNoise.sampler) SDL_ReleaseGPUSampler(_device, ssaoNoise.sampler); ssaoNoise.sampler = nullptr; - if (blurPipeline) - SDL_ReleaseGPUGraphicsPipeline(_device, blurPipeline); - blurPipeline = nullptr; _device = nullptr; } + pipeline.release(); + blurPipeline.release(); ssaoMap.destroy(); ssaoNoise.tex.destroy(); blurPass1Tex.destroy(); diff --git a/src/candlewick/posteffects/SSAO.h b/src/candlewick/posteffects/SSAO.h index 4dbf81b9..87a66c8c 100644 --- a/src/candlewick/posteffects/SSAO.h +++ b/src/candlewick/posteffects/SSAO.h @@ -1,6 +1,6 @@ #pragma once -#include "../core/Core.h" +#include "../core/GraphicsPipeline.h" #include "../core/Texture.h" #include @@ -11,7 +11,7 @@ namespace ssao { SDL_GPUTexture *inDepthMap = nullptr; SDL_GPUTexture *inNormalMap = nullptr; SDL_GPUSampler *texSampler = nullptr; - SDL_GPUGraphicsPipeline *pipeline = nullptr; + GraphicsPipeline pipeline{NoInit}; Texture ssaoMap{NoInit}; struct SsaoNoise { Texture tex{NoInit}; @@ -19,7 +19,7 @@ namespace ssao { // The texture will be N x N where N is this value. Uint32 pixel_window_size; } ssaoNoise; - SDL_GPUGraphicsPipeline *blurPipeline = nullptr; + GraphicsPipeline blurPipeline{NoInit}; // first blur pass target Texture blurPass1Tex{NoInit}; diff --git a/src/candlewick/posteffects/ScreenSpaceShadows.cpp b/src/candlewick/posteffects/ScreenSpaceShadows.cpp index fa0adfbd..78996a38 100644 --- a/src/candlewick/posteffects/ScreenSpaceShadows.cpp +++ b/src/candlewick/posteffects/ScreenSpaceShadows.cpp @@ -12,9 +12,8 @@ namespace candlewick { namespace effects { ScreenSpaceShadowPass::ScreenSpaceShadowPass(const RenderContext &renderer, const Config &config) - : config(config) { + : config(config), depthTexture(renderer.depthTarget()) { const Device &device = renderer.device; - this->depthTexture = renderer.depth_texture; auto vertexShader = Shader::fromMetadata(device, "ShadowCast.vert"); auto fragmentShader = diff --git a/src/candlewick/primitives/Heightfield.cpp b/src/candlewick/primitives/Heightfield.cpp index 8d473de4..f8cece01 100644 --- a/src/candlewick/primitives/Heightfield.cpp +++ b/src/candlewick/primitives/Heightfield.cpp @@ -7,12 +7,14 @@ namespace candlewick { MeshData loadHeightfield(const Eigen::Ref &heights, const Eigen::Ref &xgrid, const Eigen::Ref &ygrid) { - SDL_assert(heights.rows() == xgrid.size()); - SDL_assert(heights.cols() == ygrid.size()); + CANDLEWICK_ASSERT( + heights.rows() == xgrid.size(), + "Incompatible dimensions between x-grid and 'heights' matrix."); + CANDLEWICK_ASSERT( + heights.cols() == ygrid.size(), + "Incompatible dimensions between y-grid and 'heights' matrix."); const auto nx = (Sint32)heights.rows(); const auto ny = (Sint32)heights.cols(); - SDL_assert(nx > 0); - SDL_assert(ny > 0); Uint32 vertexCount = Uint32(nx * ny); std::vector vertexData; std::vector indexData; diff --git a/src/candlewick/utils/LoadMesh.cpp b/src/candlewick/utils/LoadMesh.cpp index 90300100..1ccc04a6 100644 --- a/src/candlewick/utils/LoadMesh.cpp +++ b/src/candlewick/utils/LoadMesh.cpp @@ -14,7 +14,7 @@ namespace candlewick { MeshData loadAiMesh(const aiMesh *inMesh, const aiMatrix4x4 transform) { using IndexType = MeshData::IndexType; - const Uint32 expectedFaceSize = 3; + constexpr Uint32 expectedFaceSize = 3; std::vector vertexData; std::vector indexData; @@ -42,7 +42,8 @@ MeshData loadAiMesh(const aiMesh *inMesh, const aiMatrix4x4 transform) { for (Uint32 face_id = 0; face_id < inMesh->mNumFaces; face_id++) { const aiFace &f = inMesh->mFaces[face_id]; - SDL_assert(f.mNumIndices == expectedFaceSize); + CANDLEWICK_ASSERT(f.mNumIndices == expectedFaceSize, + "Invalid number of indices in aiFace (expected 3)"); for (Uint32 ii = 0; ii < f.mNumIndices; ii++) { indexData[face_id * expectedFaceSize + ii] = f.mIndices[ii]; } diff --git a/src/candlewick/utils/VideoRecorder.cpp b/src/candlewick/utils/VideoRecorder.cpp index bcd4ebd1..2d3ea168 100644 --- a/src/candlewick/utils/VideoRecorder.cpp +++ b/src/candlewick/utils/VideoRecorder.cpp @@ -2,6 +2,7 @@ #include "WriteTextureToImage.h" #include "../core/errors.h" #include "../core/Device.h" +#include "../core/Texture.h" #include #include @@ -298,11 +299,11 @@ namespace media { VideoRecorder::~VideoRecorder() = default; - void VideoRecorder::writeTextureToVideoFrame(CommandBuffer &command_buffer, - const Device &device, - TransferBufferPool &pool, - SDL_GPUTexture *texture, - SDL_GPUTextureFormat format) { + void VideoRecorder::writeTextureToFrame(CommandBuffer &command_buffer, + const Device &device, + TransferBufferPool &pool, + SDL_GPUTexture *texture, + SDL_GPUTextureFormat format) { auto res = downloadTexture(command_buffer, device, pool, texture, format, Uint16(m_width), Uint16(m_height)); @@ -315,5 +316,13 @@ namespace media { SDL_UnmapGPUTransferBuffer(device, res.buffer); } + void VideoRecorder::writeTextureToFrame(CommandBuffer &command_buffer, + const Device &device, + TransferBufferPool &pool, + const Texture &texture) { + this->writeTextureToFrame(command_buffer, device, pool, texture, + texture.format()); + } + } // namespace media } // namespace candlewick diff --git a/src/candlewick/utils/VideoRecorder.h b/src/candlewick/utils/VideoRecorder.h index 5f6cc0c5..25b1cbda 100644 --- a/src/candlewick/utils/VideoRecorder.h +++ b/src/candlewick/utils/VideoRecorder.h @@ -74,11 +74,14 @@ namespace media { ~VideoRecorder(); - void writeTextureToVideoFrame(CommandBuffer &command_buffer, - const Device &device, - TransferBufferPool &pool, - SDL_GPUTexture *texture, - SDL_GPUTextureFormat format); + void writeTextureToFrame(CommandBuffer &command_buffer, + const Device &device, TransferBufferPool &pool, + SDL_GPUTexture *texture, + SDL_GPUTextureFormat format); + + void writeTextureToFrame(CommandBuffer &command_buffer, + const Device &device, TransferBufferPool &pool, + const Texture &texture); }; } // namespace media diff --git a/src/candlewick/utils/WriteTextureToImage.h b/src/candlewick/utils/WriteTextureToImage.h index 3531d431..28330fc2 100644 --- a/src/candlewick/utils/WriteTextureToImage.h +++ b/src/candlewick/utils/WriteTextureToImage.h @@ -32,6 +32,7 @@ namespace media { /// \brief Download texture to a mapped buffer. /// /// \warning The user is expected to unmap the buffer in the result struct. + /// \warning Calling this function will submit the provided command buffer. DownloadResult downloadTexture(CommandBuffer &command_buffer, const Device &device, TransferBufferPool &pool, SDL_GPUTexture *texture, diff --git a/src/fonts/.clang-format-ignore b/src/fonts/.clang-format-ignore new file mode 100644 index 00000000..2b39246a --- /dev/null +++ b/src/fonts/.clang-format-ignore @@ -0,0 +1 @@ +./*.cpp