diff --git a/main.cpp b/main.cpp index c5bcb27..ceb8407 100644 --- a/main.cpp +++ b/main.cpp @@ -1,4 +1,6 @@ // main.cpp +// Created by wsqsy on 11/06/2025. +// #include #include #include @@ -19,17 +21,6 @@ import sdl_wrapper; -// the vertex input layout -struct Vertex -{ - float x, y, z; // vec3 position - float r, g, b, a; // vec4 color - - /// Returns a pointer to the first component of the position (x). - /// This can be passed to ImGui::DragFloat3 etc. - float* position() { return &x; } -}; - struct CameraUniform { std::array m{}; @@ -39,22 +30,13 @@ class UserApp : public sopho::App { // GPU + resources std::shared_ptr m_gpu{}; - std::expected m_vertex_buffer{ - std::unexpected(sopho::GpuError::UNINITIALIZED)}; - std::expected m_pipeline_wrapper{ - std::unexpected(sopho::GpuError::UNINITIALIZED)}; + + std::shared_ptr m_renderable{}; // camera state float yaw = 0.0f; float pitch = 0.0f; - // a list of vertices - std::array vertices{ - Vertex{0.0F, 0.5F, 0.0F, 1.0F, 0.0F, 0.0F, 1.0F}, // top vertex - Vertex{-0.5F, -0.5F, 0.0F, 1.0F, 1.0F, 0.0F, 1.0F}, // bottom left vertex - Vertex{0.5F, -0.5F, 0.0F, 1.0F, 0.0F, 1.0F, 1.0F} // bottom right vertex - }; - CameraUniform cam{}; std::string vertex_source = @@ -72,7 +54,7 @@ layout(std140, set = 1, binding = 0) uniform Camera void main() { gl_Position = uView * vec4(a_position, 1.0f); - v_color = a_color; + v_color = vec4(1); })WSQ"; std::string fragment_source = @@ -88,11 +70,13 @@ void main() public: /** - * @brief Initialize application resources, GPU pipeline, vertex data, and Dear ImGui. + * @brief Initialize application GPU resources, shaders, vertex data, camera, and Dear ImGui. + * + * Performs creation of the GPU wrapper and render procedural, compiles and submits the vertex + * and fragment shaders, creates and uploads initial render data, sets the camera uniform to the + * identity matrix, and initializes Dear ImGui with SDL3 and SDLGPU backends. * - * Creates the GPU device, window/pipeline/buffer wrappers, compiles shaders, - * uploads the initial vertex data, initializes the camera to identity, - * and sets up Dear ImGui and its SDL3/SDLGPU backends. + * @return SDL_AppResult `SDL_APP_CONTINUE` on successful initialization, `SDL_APP_FAILURE` on error. */ SDL_AppResult init(int argc, char** argv) override { @@ -106,31 +90,20 @@ void main() } m_gpu = std::move(gpu_result.value()); - // 2. Create vertex buffer. - m_vertex_buffer = - m_gpu->create_buffer(SDL_GPU_BUFFERUSAGE_VERTEX, static_cast(sizeof(vertices))); - if (!m_vertex_buffer) - { - SDL_LogError(SDL_LOG_CATEGORY_ERROR, "Failed to create vertex buffer, error = %d", - static_cast(m_vertex_buffer.error())); - return SDL_APP_FAILURE; - } - - // 3. Create pipeline wrapper. - auto pw_result = m_gpu->create_pipeline_wrapper(); + // 2. Create pipeline wrapper. + auto pw_result = m_gpu->create_render_procedural(); if (!pw_result) { SDL_LogError(SDL_LOG_CATEGORY_ERROR, "Failed to create pipeline wrapper, error = %d", static_cast(pw_result.error())); return SDL_APP_FAILURE; } - m_pipeline_wrapper.emplace(std::move(pw_result.value())); // 4. Compile shaders and build initial pipeline. auto pipeline_init = - m_pipeline_wrapper.and_then([&](auto& pipeline) { return pipeline.set_vertex_shader(vertex_source); }) - .and_then([&](std::monostate) { return m_pipeline_wrapper->set_fragment_shader(fragment_source); }) - .and_then([&](std::monostate) { return m_pipeline_wrapper->submit(); }); + pw_result.and_then([&](auto& pipeline) { return pipeline.set_vertex_shader(vertex_source); }) + .and_then([&](std::monostate) { return pw_result->set_fragment_shader(fragment_source); }) + .and_then([&](std::monostate) { return pw_result->submit(); }); if (!pipeline_init) { @@ -139,10 +112,17 @@ void main() return SDL_APP_FAILURE; } + // 3. Create vertex buffer. + auto render_data = std::move(m_gpu->create_data(pw_result.value(), 3)); + if (!render_data) + { + SDL_LogError(SDL_LOG_CATEGORY_ERROR, "Failed to create vertex buffer, error = %d", + static_cast(render_data.error())); + return SDL_APP_FAILURE; + } + // 5. Upload initial vertex data. - auto upload_result = m_vertex_buffer.and_then( - [&](auto& vertex_buffer) - { return vertex_buffer.upload(vertices.data(), static_cast(sizeof(vertices)), 0); }); + auto upload_result = render_data.and_then([&](auto& vertex_buffer) { return vertex_buffer.upload(); }); if (!upload_result) { @@ -151,6 +131,10 @@ void main() return SDL_APP_FAILURE; } + m_renderable = std::make_shared(sopho::Renderable{ + .m_render_procedural = std::make_shared(std::move(pw_result.value())), + .m_render_data = std::make_shared(std::move(render_data.value()))}); + // 6. Initialize camera matrix to identity. { cam.m.fill(0.0F); @@ -210,7 +194,18 @@ void main() } /** - * @brief Advance the UI frame, present editors for triangle vertices and shader sources. + * @brief Advance the UI frame and present editors for vertex data and shader sources. + * + * Displays the ImGui demo and an "Editor" window with three modes: + * - Node/Vertex editing: exposes per-vertex attributes for editing and uploads the vertex buffer when modified. + * - Vertex shader editing: allows editing the vertex GLSL source and applies it to the procedural pipeline when + * changed. + * - Fragment shader editing: allows editing the fragment GLSL source and applies it to the procedural pipeline when + * changed. + * + * Any failures to upload vertex data or update shaders are logged. + * + * @return SDL_AppResult SDL_APP_CONTINUE to indicate the application should continue running. */ SDL_AppResult tick() { @@ -227,21 +222,36 @@ void main() switch (current) { - case 0: // Vertex positions + case 0: // Vertex Edit { bool changed = false; - changed = ImGui::DragFloat3("##node1", vertices[0].position(), 0.01f, -1.f, 1.f) || changed; - changed = ImGui::DragFloat3("##node2", vertices[1].position(), 0.01f, -1.f, 1.f) || changed; - changed = ImGui::DragFloat3("##node3", vertices[2].position(), 0.01f, -1.f, 1.f) || changed; + auto editor_data = m_renderable->data()->vertex_view(); + auto ptr = editor_data.raw; + for (int vertex_index = 0; vertex_index < editor_data.vertex_count; ++vertex_index) + { + for (const auto& format : editor_data.layout.get_vertex_format()) + { + switch (format) + { + case SDL_GPU_VERTEXELEMENTFORMAT_FLOAT3: + changed |= ImGui::DragFloat3(std::format("node{}", vertex_index).data(), + reinterpret_cast(ptr), 0.01f, -1.f, 1.f); + break; + case SDL_GPU_VERTEXELEMENTFORMAT_FLOAT4: + changed |= ImGui::DragFloat4(std::format("color{}", vertex_index).data(), + reinterpret_cast(ptr), 0.01f, -1.f, 1.f); + break; + default: + break; + } + auto size = sopho::get_size(format); + ptr += size; + } + } if (changed) { - auto upload_result = m_vertex_buffer.and_then( - [&](auto& vertex_buffer) - { - return vertex_buffer.upload(vertices.data(), static_cast(sizeof(vertices)), - 0); - }); + auto upload_result = m_renderable->data()->upload(); if (!upload_result) { SDL_LogError(SDL_LOG_CATEGORY_GPU, "Failed to upload vertex buffer in tick(), error = %d", @@ -260,8 +270,7 @@ void main() if (ImGui::InputTextMultiline("##vertex editor", &vertex_source, size, ImGuiInputTextFlags_AllowTabInput)) { - auto result = m_pipeline_wrapper.and_then( - [&](auto& pipeline_wrapper) { return pipeline_wrapper.set_vertex_shader(vertex_source); }); + auto result = m_renderable->procedural()->set_vertex_shader(vertex_source); if (!result) { SDL_LogError(SDL_LOG_CATEGORY_RENDER, "Failed to set vertex shader from editor, error = %d", @@ -280,8 +289,7 @@ void main() if (ImGui::InputTextMultiline("##fragment editor", &fragment_source, size, ImGuiInputTextFlags_AllowTabInput)) { - auto result = m_pipeline_wrapper.and_then( - [&](auto& pipeline_wrapper) { return pipeline_wrapper.set_fragment_shader(fragment_source); }); + auto result = m_renderable->procedural()->set_fragment_shader(fragment_source); if (!result) { SDL_LogError(SDL_LOG_CATEGORY_RENDER, "Failed to set fragment shader from editor, error = %d", @@ -301,7 +309,13 @@ void main() } /** - * @brief Render the triangle and ImGui UI to the GPU and present the swapchain frame. + * @brief Render the scene (triangle and ImGui) into the current swapchain image and present it. + * + * Performs pipeline submission if needed, prepares ImGui draw data, records GPU commands + * to clear and render the color target, uploads the camera uniform, binds vertex buffers + * and the graphics pipeline, issues the draw call, renders ImGui, and submits the command buffer. + * + * @return SDL_AppResult `SDL_APP_CONTINUE` to keep the application running. */ SDL_AppResult draw() { @@ -309,8 +323,7 @@ void main() ImDrawData* draw_data = ImGui::GetDrawData(); // Rebuild pipeline if needed. - auto pipeline_submit = - m_pipeline_wrapper.and_then([](auto& pipeline_wrapper) { return pipeline_wrapper.submit(); }); + auto pipeline_submit = m_renderable->procedural()->submit(); if (!pipeline_submit) { SDL_LogError(SDL_LOG_CATEGORY_GPU, "Pipeline submit failed, error = %d", @@ -370,12 +383,8 @@ void main() SDL_GPURenderPass* renderPass = SDL_BeginGPURenderPass(commandBuffer, &colorTargetInfo, 1, nullptr); // Bind pipeline if available. - m_pipeline_wrapper.and_then( - [&](auto& pipeline_wrapper) -> std::expected - { - SDL_BindGPUGraphicsPipeline(renderPass, pipeline_wrapper.data()); - return std::monostate{}; - }); + + SDL_BindGPUGraphicsPipeline(renderPass, m_renderable->procedural()->data()); // Compute camera matrix and upload as a vertex uniform. { @@ -407,17 +416,8 @@ void main() SDL_PushGPUVertexUniformData(commandBuffer, 0, cam.m.data(), static_cast(sizeof(cam.m))); } - // Bind vertex buffer and draw. - m_vertex_buffer.and_then( - [&](auto& vertex_buffer) -> std::expected - { - SDL_GPUBufferBinding bufferBindings[1]{}; - bufferBindings[0].buffer = vertex_buffer.data(); - bufferBindings[0].offset = 0; - - SDL_BindGPUVertexBuffers(renderPass, 0, bufferBindings, 1); - return std::monostate{}; - }); + SDL_BindGPUVertexBuffers(renderPass, 0, m_renderable->data()->get_buffer_binding().data(), + m_renderable->data()->get_buffer_binding().size()); SDL_DrawGPUPrimitives(renderPass, 3, 1, 0, 0); diff --git a/sdl_wrapper/CMakeLists.txt b/sdl_wrapper/CMakeLists.txt index 935e679..6d3c439 100644 --- a/sdl_wrapper/CMakeLists.txt +++ b/sdl_wrapper/CMakeLists.txt @@ -10,12 +10,15 @@ target_sources(sdl_wrapper sdl_wrapper.ixx sdl_wrapper.buffer.ixx sdl_wrapper.app.ixx - sdl_wrapper.pipeline.ixx + sdl_wrapper.render_procedural.ixx + sdl_wrapper.render_data.ixx + sdl_wrapper.renderable.ixx sdl_wrapper.gpu.ixx sdl_wrapper.decl.ixx + sdl_wrapper.vertex_layout.ixx PRIVATE sdl_wrapper.buffer.cpp - sdl_wrapper.pipeline.cpp + sdl_wrapper.render_procedural.cpp sdl_wrapper.gpu.cpp sdl_callback_implement.cpp ) diff --git a/sdl_wrapper/sdl_wrapper.buffer.cpp b/sdl_wrapper/sdl_wrapper.buffer.cpp index 7d2b61c..78e9323 100644 --- a/sdl_wrapper/sdl_wrapper.buffer.cpp +++ b/sdl_wrapper/sdl_wrapper.buffer.cpp @@ -16,6 +16,12 @@ import :gpu; namespace sopho { + /** + * @brief Releases any owned GPU resources held by the wrapper. + * + * Ensures the associated GPU object is valid, then releases the GPU buffer and the + * GPU transfer buffer if they exist and clears their pointers. + */ BufferWrapper::~BufferWrapper() noexcept { if (!m_gpu) @@ -23,11 +29,11 @@ namespace sopho return; } - // Release vertex buffer - if (m_vertex_buffer) + // Release gpu buffer + if (m_gpu_buffer) { - m_gpu->release_buffer(m_vertex_buffer); - m_vertex_buffer = nullptr; + m_gpu->release_buffer(m_gpu_buffer); + m_gpu_buffer = nullptr; } // Release transfer buffer @@ -35,51 +41,28 @@ namespace sopho { SDL_ReleaseGPUTransferBuffer(m_gpu->device(), m_transfer_buffer); m_transfer_buffer = nullptr; - m_transfer_buffer_size = 0; } } - [[nodiscard]] std::expected - BufferWrapper::upload(const void* src_data, std::uint32_t size, std::uint32_t offset) + /** + * @brief Uploads the internal CPU-side buffer to the GPU buffer via the transfer buffer and a GPU copy pass. + * + * Copies the contents of m_cpu_buffer into the existing transfer buffer, records a GPU copy pass that + * uploads that transfer buffer into m_gpu_buffer, submits the command buffer, and returns success status. + * + * @returns std::monostate on success; otherwise an unexpected `GpuError` indicating the failure: + * - `GpuError::MAP_TRANSFER_BUFFER_FAILED` if mapping the transfer buffer failed. + * - `GpuError::ACQUIRE_COMMAND_BUFFER_FAILED` if acquiring a GPU command buffer failed. + * - `GpuError::BEGIN_COPY_PASS_FAILED` if beginning the GPU copy pass failed. + * - `GpuError::SUBMIT_COMMAND_FAILED` if submitting the GPU command buffer failed. + */ + [[nodiscard]] std::expected BufferWrapper::upload() { - // Bounds check to avoid writing past the end of the GPU buffer. - if (offset + size > m_vertex_buffer_size) - { - SDL_LogError(SDL_LOG_CATEGORY_GPU, "%s:%d buffer overflow: size=%u, offset=%u, buffer_size=%u", __FILE__, - __LINE__, size, offset, m_vertex_buffer_size); - - return std::unexpected(GpuError::BUFFER_OVERFLOW); - } + auto src_data = m_cpu_buffer.data(); + auto size = m_cpu_buffer.size(); auto* device = m_gpu->device(); - // 1. Ensure the transfer buffer capacity is sufficient. - if (size > m_transfer_buffer_size) - { - if (m_transfer_buffer != nullptr) - { - SDL_ReleaseGPUTransferBuffer(device, m_transfer_buffer); - m_transfer_buffer = nullptr; - m_transfer_buffer_size = 0; - } - - SDL_GPUTransferBufferCreateInfo transfer_info{}; - transfer_info.usage = SDL_GPU_TRANSFERBUFFERUSAGE_UPLOAD; - transfer_info.size = size; - transfer_info.props = 0; - - m_transfer_buffer = SDL_CreateGPUTransferBuffer(device, &transfer_info); - if (!m_transfer_buffer) - { - SDL_LogError(SDL_LOG_CATEGORY_GPU, "%s:%d failed to create transfer buffer: %s", __FILE__, __LINE__, - SDL_GetError()); - - return std::unexpected(GpuError::CREATE_TRANSFER_BUFFER_FAILED); - } - - m_transfer_buffer_size = transfer_info.size; - } - // 2. Map the transfer buffer and copy data into it. void* dst = SDL_MapGPUTransferBuffer(device, m_transfer_buffer, false); if (!dst) @@ -118,9 +101,9 @@ namespace sopho location.offset = 0; SDL_GPUBufferRegion region{}; - region.buffer = m_vertex_buffer; + region.buffer = m_gpu_buffer; region.size = size; - region.offset = offset; + region.offset = 0; SDL_UploadToGPUBuffer(copy_pass, &location, ®ion, false); diff --git a/sdl_wrapper/sdl_wrapper.buffer.ixx b/sdl_wrapper/sdl_wrapper.buffer.ixx index 22daa4b..6f7fd39 100644 --- a/sdl_wrapper/sdl_wrapper.buffer.ixx +++ b/sdl_wrapper/sdl_wrapper.buffer.ixx @@ -1,12 +1,11 @@ // sdl_wrapper.buffer.ixx // Created by sophomore on 11/8/25. // - module; - #include #include #include +#include #include "SDL3/SDL_gpu.h" @@ -18,15 +17,17 @@ export namespace sopho class BufferWrapper { std::shared_ptr m_gpu{}; // Owns the device lifetime - SDL_GPUBuffer* m_vertex_buffer{}; // Target GPU buffer + SDL_GPUBuffer* m_gpu_buffer{}; // Target GPU buffer SDL_GPUTransferBuffer* m_transfer_buffer{}; // Staging/transfer buffer - std::uint32_t m_vertex_buffer_size{}; // Total size of the GPU buffer - std::uint32_t m_transfer_buffer_size{}; // Current capacity of the transfer buffer + std::uint32_t m_buffer_size{}; // Total size of the GPU buffer + std::vector m_cpu_buffer{}; // Only GpuWrapper is allowed to construct this type. - BufferWrapper(std::shared_ptr gpu, SDL_GPUBuffer* buffer, std::uint32_t size) noexcept : - m_gpu(std::move(gpu)), m_vertex_buffer(buffer), m_vertex_buffer_size(size) + BufferWrapper(std::shared_ptr gpu, SDL_GPUBuffer* gpu_buffer, + SDL_GPUTransferBuffer* transfer_buffer, std::uint32_t size) noexcept : + m_gpu(std::move(gpu)), m_gpu_buffer(gpu_buffer), m_transfer_buffer(transfer_buffer), m_buffer_size(size) { + m_cpu_buffer.resize(m_buffer_size); } public: @@ -35,20 +36,13 @@ export namespace sopho BufferWrapper(BufferWrapper&&) = default; BufferWrapper& operator=(BufferWrapper&&) = default; - /// Upload a block of data into the GPU buffer at the given byte offset. - /// - /// This function may fail in several ways: - /// - The requested upload range exceeds the buffer size. - /// - The transfer buffer cannot be created or resized. - /// - Mapping the transfer buffer fails. - /// - Acquiring a GPU command buffer fails. - /// - /// All such failures are reported via the returned std::expected. - [[nodiscard]] std::expected upload(const void* data, std::uint32_t size, - std::uint32_t offset); + /// Upload CPU buffer into the GPU buffer at the given byte offset. + + [[nodiscard]] std::expected upload(); /// Returns the underlying SDL_GPUBuffer pointer. - [[nodiscard]] SDL_GPUBuffer* data() const noexcept { return m_vertex_buffer; } + [[nodiscard]] SDL_GPUBuffer* gpu_buffer() const noexcept { return m_gpu_buffer; } + [[nodiscard]] auto cpu_buffer() noexcept { return m_cpu_buffer.data(); } ~BufferWrapper() noexcept; diff --git a/sdl_wrapper/sdl_wrapper.decl.ixx b/sdl_wrapper/sdl_wrapper.decl.ixx index f2ce808..9bc1078 100644 --- a/sdl_wrapper/sdl_wrapper.decl.ixx +++ b/sdl_wrapper/sdl_wrapper.decl.ixx @@ -12,7 +12,7 @@ export namespace sopho CREATE_DEVICE_FAILED, CREATE_WINDOW_FAILED, CLAIM_WINDOW_FAILED, - CREATE_BUFFER_FAILED, + CREATE_GPU_BUFFER_FAILED, CREATE_TRANSFER_BUFFER_FAILED, CREATE_SHADER_FAILED, CREATE_PIPELINE_FAILED, @@ -30,5 +30,6 @@ export namespace sopho class App; class GpuWrapper; class BufferWrapper; - class PipelineWrapper; + class RenderProcedural; + class RenderData; } diff --git a/sdl_wrapper/sdl_wrapper.gpu.cpp b/sdl_wrapper/sdl_wrapper.gpu.cpp index 7bdd5d2..fffc366 100644 --- a/sdl_wrapper/sdl_wrapper.gpu.cpp +++ b/sdl_wrapper/sdl_wrapper.gpu.cpp @@ -1,4 +1,4 @@ -// sdl_wrapper.gpu.cpp +// sdl_wrapper.gpu.cpp // Created by wsqsy on 11/14/2025. // module; @@ -7,24 +7,75 @@ module; #include "SDL3/SDL_log.h" module sdl_wrapper; import :gpu; -import :pipeline; +import :render_procedural; +import :render_data; +import :vertex_layout; namespace sopho { + /** + * @brief Create a GPU buffer and its associated upload transfer buffer. + * + * @param flag Usage flags for the GPU buffer. + * @param size Size in bytes of the buffer to allocate. + * @return std::expected BufferWrapper containing the created GPU buffer, its transfer + * buffer, and the buffer size on success; `std::unexpected` with a `GpuError` on failure. + */ std::expected GpuWrapper::create_buffer(SDL_GPUBufferUsageFlags flag, uint32_t size) { SDL_GPUBufferCreateInfo create_info{.usage = flag, .size = size}; - auto buffer = SDL_CreateGPUBuffer(device(), &create_info); - if (!buffer) + auto gpu_buffer = SDL_CreateGPUBuffer(device(), &create_info); + if (!gpu_buffer) { SDL_LogError(SDL_LOG_CATEGORY_GPU, "%s:%d %s", __FILE__, __LINE__, SDL_GetError()); - return std::unexpected(GpuError::CREATE_BUFFER_FAILED); + return std::unexpected(GpuError::CREATE_GPU_BUFFER_FAILED); + } + SDL_GPUTransferBufferCreateInfo transfer_info{}; + transfer_info.usage = SDL_GPU_TRANSFERBUFFERUSAGE_UPLOAD; + transfer_info.size = size; + transfer_info.props = 0; + auto transfer_buffer = SDL_CreateGPUTransferBuffer(device(), &transfer_info); + if (!transfer_buffer) + { + SDL_LogError(SDL_LOG_CATEGORY_GPU, "%s:%d %s", __FILE__, __LINE__, SDL_GetError()); + return std::unexpected(GpuError::CREATE_TRANSFER_BUFFER_FAILED); + } + return BufferWrapper{shared_from_this(), gpu_buffer, transfer_buffer, size}; + } + /** + * @brief Create RenderData for a procedural render setup and given vertex count. + * + * Allocates a GPU vertex buffer sized to hold `vertex_count` vertices using the + * vertex layout from `render_procedural`, and returns a RenderData that owns + * the allocated buffer, the vertex layout, and the vertex count. If buffer + * creation fails, returns the corresponding GpuError. + * + * @param render_procedural Source procedural that provides the vertex layout. + * @param vertex_count Number of vertices the allocated buffer must hold. + * @return RenderData RenderData containing the allocated vertex buffer, the vertex layout, and `vertex_count`. + */ + std::expected GpuWrapper::create_data(const RenderProcedural& render_procedural, + uint32_t vertex_count) + { + auto size = render_procedural.vertex_layout().get_stride() * vertex_count; + auto buffer = create_buffer(SDL_GPU_BUFFERUSAGE_VERTEX, size); + if (!buffer) + { + return std::unexpected(buffer.error()); } - return BufferWrapper{shared_from_this(), buffer, size}; + return RenderData{std::move(buffer.value()), render_procedural.vertex_layout(), vertex_count}; } - std::expected GpuWrapper::create_pipeline_wrapper() + /** + * @brief Create a RenderProcedural configured for the device's texture format. + * + * Queries the GPU's texture format and constructs a RenderProcedural associated with this GpuWrapper. + * + * @return std::expected Contains the constructed RenderProcedural on success, or an + * unexpected holding the corresponding GpuError on failure. + */ + std::expected GpuWrapper::create_render_procedural() { - // Query texture format, then construct PipelineWrapper + // Query texture format, then construct RenderProcedural return get_texture_format().transform([self = shared_from_this()](SDL_GPUTextureFormat format) - { return PipelineWrapper{self, format}; }); + { return RenderProcedural{self, format}; }); } } // namespace sopho diff --git a/sdl_wrapper/sdl_wrapper.gpu.ixx b/sdl_wrapper/sdl_wrapper.gpu.ixx index e8e7137..d245bce 100644 --- a/sdl_wrapper/sdl_wrapper.gpu.ixx +++ b/sdl_wrapper/sdl_wrapper.gpu.ixx @@ -12,7 +12,7 @@ module; export module sdl_wrapper:gpu; import :decl; import :buffer; -import :pipeline; +import :render_procedural; export namespace sopho { @@ -213,6 +213,8 @@ export namespace sopho [[nodiscard]] auto window() const { return m_ctx.window.raw; } [[nodiscard]] std::expected create_buffer(SDL_GPUBufferUsageFlags flag, uint32_t size); + [[nodiscard]] std::expected + create_data(const RenderProcedural& render_procedural, uint32_t vertex_count); auto release_buffer(SDL_GPUBuffer* buffer) { @@ -222,7 +224,7 @@ export namespace sopho } } - [[nodiscard]] std::expected create_pipeline_wrapper(); + [[nodiscard]] std::expected create_render_procedural(); std::expected create_pipeline(const SDL_GPUGraphicsPipelineCreateInfo& create_info) diff --git a/sdl_wrapper/sdl_wrapper.ixx b/sdl_wrapper/sdl_wrapper.ixx index 9e7a9fd..94a2470 100644 --- a/sdl_wrapper/sdl_wrapper.ixx +++ b/sdl_wrapper/sdl_wrapper.ixx @@ -7,4 +7,7 @@ export import :decl; export import :app; export import :gpu; export import :buffer; -export import :pipeline; +export import :render_procedural; +export import :renderable; +export import :vertex_layout; +export import :render_data; diff --git a/sdl_wrapper/sdl_wrapper.render_data.ixx b/sdl_wrapper/sdl_wrapper.render_data.ixx new file mode 100644 index 0000000..cc9ad66 --- /dev/null +++ b/sdl_wrapper/sdl_wrapper.render_data.ixx @@ -0,0 +1,49 @@ +// sdl_wrapper.render_data.ixx +// Created by sophomore on 11/18/25. +// +module; +#include +#include +#include +export module sdl_wrapper:render_data; +import :buffer; +import :vertex_layout; +namespace sopho +{ + export class RenderData + { + BufferWrapper m_buffer; + VertexLayout m_layouts{}; + size_t m_vertex_count{}; + std::vector m_bindings{}; + + public: + explicit RenderData(BufferWrapper&& buffer_wrapper, const VertexLayout& layouts, size_t vertex_count) : + m_buffer(std::move(buffer_wrapper)), m_layouts(layouts), m_vertex_count(vertex_count) + { + m_bindings.emplace_back(m_buffer.gpu_buffer(), 0); + } + + public: + struct VertexView + { + VertexLayout layout{}; + size_t vertex_count{}; + std::byte* raw{}; + }; + RenderData(const RenderData&) = delete; + RenderData& operator=(const RenderData&) = delete; + RenderData(RenderData&&) = default; + RenderData& operator=(RenderData&&) = default; + auto& buffer() { return m_buffer; } + auto& get_buffer_binding() { return m_bindings; } + auto vertex_view() + { + return VertexView{.layout = m_layouts, .vertex_count = m_vertex_count, .raw = m_buffer.cpu_buffer()}; + } + auto upload() + { + return m_buffer.upload(); + } + }; +} // namespace sopho diff --git a/sdl_wrapper/sdl_wrapper.pipeline.cpp b/sdl_wrapper/sdl_wrapper.render_procedural.cpp similarity index 64% rename from sdl_wrapper/sdl_wrapper.pipeline.cpp rename to sdl_wrapper/sdl_wrapper.render_procedural.cpp index 623bd56..8c60ae2 100644 --- a/sdl_wrapper/sdl_wrapper.pipeline.cpp +++ b/sdl_wrapper/sdl_wrapper.render_procedural.cpp @@ -12,12 +12,20 @@ module; #include "SDL3/SDL_log.h" #include "shaderc/shaderc.hpp" module sdl_wrapper; -import :pipeline; +import :render_procedural; import :gpu; namespace sopho { - // Helper: convert shaderc SPIR-V result (words) into a byte buffer. + /** + * @brief Convert a shaderc SPIR-V compilation result into a contiguous byte buffer. + * + * Converts the iterable sequence of 32-bit words produced by shaderc into a + * std::vector containing the raw SPIR-V bytes in order. + * + * @param result The shaderc SPIR-V compilation result (iterable of uint32_t words). + * @return std::vector Raw SPIR-V byte sequence suitable for creating GPU shader modules. + */ static std::vector spv_result_to_bytes(const shaderc::SpvCompilationResult& result) { // shaderc::SpvCompilationResult is an iterable sequence of uint32_t words. @@ -39,34 +47,31 @@ namespace sopho return bytes; } - PipelineWrapper::PipelineWrapper(std::shared_ptr gpu, SDL_GPUTextureFormat swapchain_format) noexcept : - m_gpu(std::move(gpu)) + /** + * @brief Construct a RenderProcedural configured for the given GPU and swapchain format. + * + * Initializes default GPU pipeline state: vertex layout (positions + colors), shaderc options + * targeting Vulkan/SPIR-V, a vertex buffer description, an alpha blend color target using the + * provided swapchain format, and a default graphics pipeline create-info with no shaders. + * Marks the pipeline as modified so it will be (re)created on the next submit. + * + * @param swapchain_format Pixel format to use for the pipeline's color target. + */ + RenderProcedural::RenderProcedural(std::shared_ptr gpu, SDL_GPUTextureFormat swapchain_format) noexcept + : m_gpu(std::move(gpu)) { + m_vertex_layout.set_vertex_attributes({SDL_GPU_VERTEXELEMENTFORMAT_FLOAT3, SDL_GPU_VERTEXELEMENTFORMAT_FLOAT4}); // Configure shaderc to target Vulkan/SPIR-V. m_options.SetTargetEnvironment(shaderc_target_env_vulkan, 0); // Setup vertex buffer description. SDL_GPUVertexBufferDescription vb_desc{}; vb_desc.slot = 0; - vb_desc.pitch = 28; // 3 * float + 4 * float = 28 bytes + vb_desc.pitch = m_vertex_layout.get_stride(); vb_desc.input_rate = SDL_GPU_VERTEXINPUTRATE_VERTEX; vb_desc.instance_step_rate = 0; m_vertex_buffer_descriptions.push_back(vb_desc); - // Setup vertex attributes: position (float3) + color (float4). - SDL_GPUVertexAttribute attr_pos{}; - attr_pos.location = 0; - attr_pos.buffer_slot = 0; - attr_pos.format = SDL_GPU_VERTEXELEMENTFORMAT_FLOAT3; - attr_pos.offset = 0; - m_vertex_attributes.push_back(attr_pos); - - SDL_GPUVertexAttribute attr_color{}; - attr_color.location = 1; - attr_color.buffer_slot = 0; - attr_color.format = SDL_GPU_VERTEXELEMENTFORMAT_FLOAT4; - attr_color.offset = sizeof(float) * 3; - m_vertex_attributes.push_back(attr_color); // Setup color target blend state. SDL_GPUColorTargetBlendState blend{}; @@ -88,8 +93,9 @@ namespace sopho SDL_GPUGraphicsPipelineCreateInfo info{}; info.vertex_input_state.vertex_buffer_descriptions = m_vertex_buffer_descriptions.data(); info.vertex_input_state.num_vertex_buffers = static_cast(m_vertex_buffer_descriptions.size()); - info.vertex_input_state.vertex_attributes = m_vertex_attributes.data(); - info.vertex_input_state.num_vertex_attributes = static_cast(m_vertex_attributes.size()); + info.vertex_input_state.vertex_attributes = m_vertex_layout.get_vertex_attributes().data(); + info.vertex_input_state.num_vertex_attributes = + static_cast(m_vertex_layout.get_vertex_attributes().size()); info.primitive_type = SDL_GPU_PRIMITIVETYPE_TRIANGLELIST; @@ -104,7 +110,14 @@ namespace sopho m_modified = true; } - PipelineWrapper::~PipelineWrapper() noexcept + /** + * @brief Releases owned GPU pipeline and shader resources and clears their handles. + * + * If a GPU wrapper is available, releases the graphics pipeline, vertex shader, + * and fragment shader held by this object (if present) and resets their pointers + * to nullptr. + */ + RenderProcedural::~RenderProcedural() noexcept { if (!m_gpu) { @@ -130,7 +143,17 @@ namespace sopho } } - [[nodiscard]] std::expected PipelineWrapper::submit() + /** + * @brief Ensures the GPU graphics pipeline matches the current pipeline description, creating or replacing the + * pipeline if changes are pending. + * + * If no modifications are pending this function is a no-op. On success it updates the stored graphics pipeline and + * clears the modified flag. + * + * @return std::monostate on success; `std::unexpected` containing the GPU error if pipeline creation + * fails. + */ + [[nodiscard]] std::expected RenderProcedural::submit() { if (!m_modified) { @@ -143,9 +166,9 @@ namespace sopho m_pipeline_info.vertex_input_state.num_vertex_buffers = static_cast(m_vertex_buffer_descriptions.size()); - m_pipeline_info.vertex_input_state.vertex_attributes = m_vertex_attributes.data(); + m_pipeline_info.vertex_input_state.vertex_attributes = m_vertex_layout.get_vertex_attributes().data(); m_pipeline_info.vertex_input_state.num_vertex_attributes = - static_cast(m_vertex_attributes.size()); + static_cast(m_vertex_layout.get_vertex_attributes().size()); m_pipeline_info.target_info.color_target_descriptions = m_color_target_descriptions.data(); m_pipeline_info.target_info.num_color_targets = static_cast(m_color_target_descriptions.size()); @@ -172,7 +195,17 @@ namespace sopho return std::monostate{}; } - [[nodiscard]] std::expected PipelineWrapper::set_vertex_shader(const std::string& source) + /** + * Compile the provided GLSL vertex shader source, create a GPU vertex shader, and install it into the pipeline. + * + * This updates the stored vertex shader, sets the pipeline's vertex_shader to the new shader, and marks the + * pipeline as modified so a new graphics pipeline will be created on the next submit. + * + * @param source GLSL source code for the vertex shader. + * @return std::monostate on success; `std::unexpected(GpuError::COMPILE_VERTEX_SHADER_FAILED)` if shader + * compilation fails, or `std::unexpected()` if shader creation on the GPU fails. + */ + [[nodiscard]] std::expected RenderProcedural::set_vertex_shader(const std::string& source) { auto result = m_compiler.CompileGlslToSpv(source, shaderc_glsl_vertex_shader, "vertex.glsl", m_options); if (result.GetCompilationStatus() != shaderc_compilation_status_success) @@ -208,8 +241,19 @@ namespace sopho return std::monostate{}; } + /** + * @brief Compiles GLSL fragment shader source, creates a GPU fragment shader, and installs it into the pipeline. + * + * Compiles the provided GLSL fragment source to SPIR-V, converts the result into a byte vector, and asks the GPU + * wrapper to create a fragment shader; on success the new shader replaces any existing fragment shader in the + * object, updates the pipeline's fragment shader reference, and marks the pipeline state as modified. + * + * @param source GLSL source code for the fragment shader. + * @return std::monostate on success; `std::unexpected` on failure (compilation error or GPU + * shader-creation error). + */ [[nodiscard]] std::expected - PipelineWrapper::set_fragment_shader(const std::string& source) + RenderProcedural::set_fragment_shader(const std::string& source) { auto result = m_compiler.CompileGlslToSpv(source, shaderc_glsl_fragment_shader, "fragment.glsl", m_options); if (result.GetCompilationStatus() != shaderc_compilation_status_success) diff --git a/sdl_wrapper/sdl_wrapper.pipeline.ixx b/sdl_wrapper/sdl_wrapper.render_procedural.ixx similarity index 73% rename from sdl_wrapper/sdl_wrapper.pipeline.ixx rename to sdl_wrapper/sdl_wrapper.render_procedural.ixx index f353e2c..557c01d 100644 --- a/sdl_wrapper/sdl_wrapper.pipeline.ixx +++ b/sdl_wrapper/sdl_wrapper.render_procedural.ixx @@ -10,12 +10,12 @@ module; #include "SDL3/SDL_gpu.h" #include "shaderc/shaderc.hpp" -export module sdl_wrapper:pipeline; +export module sdl_wrapper:render_procedural; import :decl; // GpuError, forward declarations, etc. - +import :vertex_layout; export namespace sopho { - class PipelineWrapper + class RenderProcedural { std::shared_ptr m_gpu{}; @@ -24,24 +24,24 @@ export namespace sopho SDL_GPUShader* m_fragment_shader{}; std::vector m_vertex_buffer_descriptions{}; - std::vector m_vertex_attributes{}; std::vector m_color_target_descriptions{}; SDL_GPUGraphicsPipelineCreateInfo m_pipeline_info{}; shaderc::Compiler m_compiler{}; shaderc::CompileOptions m_options{}; + VertexLayout m_vertex_layout{}; bool m_modified{false}; // Internal constructor: assumes texture format is already known and valid. - PipelineWrapper(std::shared_ptr gpu, SDL_GPUTextureFormat swapchain_format) noexcept; + RenderProcedural(std::shared_ptr gpu, SDL_GPUTextureFormat swapchain_format) noexcept; public: - PipelineWrapper(const PipelineWrapper&) = delete; - PipelineWrapper& operator=(const PipelineWrapper&) = delete; - PipelineWrapper(PipelineWrapper&&) noexcept = default; - PipelineWrapper& operator=(PipelineWrapper&&) = delete; - ~PipelineWrapper() noexcept; + RenderProcedural(const RenderProcedural&) = delete; + RenderProcedural& operator=(const RenderProcedural&) = delete; + RenderProcedural(RenderProcedural&&) noexcept = default; + RenderProcedural& operator=(RenderProcedural&&) = delete; + ~RenderProcedural() noexcept; /// Returns the underlying SDL_GPUGraphicsPipeline*. [[nodiscard]] SDL_GPUGraphicsPipeline* data() const noexcept { return m_graphics_pipeline; } @@ -66,6 +66,14 @@ export namespace sopho /// On failure, a compile or creation error is returned. [[nodiscard]] std::expected set_fragment_shader(const std::string& source); + auto set_vertex_attributes(std::vector vertex_attributes) + { + m_vertex_layout.set_vertex_attributes(std::move(vertex_attributes)); + m_modified = true; + } + + auto& vertex_layout() const { return m_vertex_layout; } + friend class GpuWrapper; }; } // namespace sopho diff --git a/sdl_wrapper/sdl_wrapper.renderable.ixx b/sdl_wrapper/sdl_wrapper.renderable.ixx new file mode 100644 index 0000000..a1056d9 --- /dev/null +++ b/sdl_wrapper/sdl_wrapper.renderable.ixx @@ -0,0 +1,24 @@ +// sdl_wrapper.renderable.ixx +// Created by wsqsy on 11/19/2025. +// +module; +#include +export module sdl_wrapper:renderable; +import :decl; +namespace sopho +{ + export class Renderable + { + public: + std::shared_ptr m_render_procedural{}; + std::shared_ptr m_render_data{}; + auto & procedural() + { + return m_render_procedural; + } + auto & data() + { + return m_render_data; + } + }; +} diff --git a/sdl_wrapper/sdl_wrapper.vertex_layout.ixx b/sdl_wrapper/sdl_wrapper.vertex_layout.ixx new file mode 100644 index 0000000..8ce07c6 --- /dev/null +++ b/sdl_wrapper/sdl_wrapper.vertex_layout.ixx @@ -0,0 +1,111 @@ +// sdl_wrapper.vertex_attribute.ixx +// Created by wsqsy on 11/17/2025. +// +module; +#include +#include +#include "SDL3/SDL_gpu.h" +export module sdl_wrapper:vertex_layout; + +namespace sopho +{ + + export std::uint32_t get_size(SDL_GPUVertexElementFormat format) + { + switch (format) + { + default: + case SDL_GPU_VERTEXELEMENTFORMAT_INVALID: + return 0; + case SDL_GPU_VERTEXELEMENTFORMAT_INT: + return 4; + case SDL_GPU_VERTEXELEMENTFORMAT_INT2: + return 8; + case SDL_GPU_VERTEXELEMENTFORMAT_INT3: + return 12; + case SDL_GPU_VERTEXELEMENTFORMAT_INT4: + return 16; + case SDL_GPU_VERTEXELEMENTFORMAT_UINT: + return 4; + case SDL_GPU_VERTEXELEMENTFORMAT_UINT2: + return 8; + case SDL_GPU_VERTEXELEMENTFORMAT_UINT3: + return 12; + case SDL_GPU_VERTEXELEMENTFORMAT_UINT4: + return 16; + case SDL_GPU_VERTEXELEMENTFORMAT_FLOAT: + return 4; + case SDL_GPU_VERTEXELEMENTFORMAT_FLOAT2: + return 8; + case SDL_GPU_VERTEXELEMENTFORMAT_FLOAT3: + return 12; + case SDL_GPU_VERTEXELEMENTFORMAT_FLOAT4: + return 16; + case SDL_GPU_VERTEXELEMENTFORMAT_BYTE2: + return 2; + case SDL_GPU_VERTEXELEMENTFORMAT_BYTE4: + return 4; + case SDL_GPU_VERTEXELEMENTFORMAT_UBYTE2: + return 2; + case SDL_GPU_VERTEXELEMENTFORMAT_UBYTE4: + return 4; + case SDL_GPU_VERTEXELEMENTFORMAT_BYTE2_NORM: + return 2; + case SDL_GPU_VERTEXELEMENTFORMAT_BYTE4_NORM: + return 4; + case SDL_GPU_VERTEXELEMENTFORMAT_UBYTE2_NORM: + return 2; + case SDL_GPU_VERTEXELEMENTFORMAT_UBYTE4_NORM: + return 4; + case SDL_GPU_VERTEXELEMENTFORMAT_SHORT2: + return 4; + case SDL_GPU_VERTEXELEMENTFORMAT_SHORT4: + return 8; + case SDL_GPU_VERTEXELEMENTFORMAT_USHORT2: + return 4; + case SDL_GPU_VERTEXELEMENTFORMAT_USHORT4: + return 8; + case SDL_GPU_VERTEXELEMENTFORMAT_SHORT2_NORM: + return 4; + case SDL_GPU_VERTEXELEMENTFORMAT_SHORT4_NORM: + return 8; + case SDL_GPU_VERTEXELEMENTFORMAT_USHORT2_NORM: + return 4; + case SDL_GPU_VERTEXELEMENTFORMAT_USHORT4_NORM: + return 8; + case SDL_GPU_VERTEXELEMENTFORMAT_HALF2: + return 4; + case SDL_GPU_VERTEXELEMENTFORMAT_HALF4: + return 8; + } + } + + class VertexLayout + { + std::vector raw{}; + std::vector attributes{}; + uint32_t stride = 0; + + public: + auto set_vertex_attributes(std::vector vertex_attributes) + { + raw.swap(vertex_attributes); + stride = 0; + attributes.clear(); + for (int i = 0; i < raw.size(); ++i) + { + SDL_GPUVertexAttribute vertex_attribute{}; + vertex_attribute.location = i; + vertex_attribute.buffer_slot = 0; + vertex_attribute.format = raw[i]; + vertex_attribute.offset = stride; + stride += get_size(vertex_attribute.format); + attributes.push_back(vertex_attribute); + } + } + + const auto& get_vertex_format() { return raw; } + const auto& get_vertex_attributes() { return attributes; } + auto get_stride() const { return stride; } + }; +} // namespace sopho