diff --git a/main.cpp b/main.cpp index b053b87..c5bcb27 100644 --- a/main.cpp +++ b/main.cpp @@ -1,9 +1,12 @@ +// main.cpp #include #include #include +#include #include +#include #include -#include "shaderc/shaderc.hpp" +#include #include "imgui.h" #include "imgui_impl_sdl3.h" @@ -13,19 +16,18 @@ #include "SDL3/SDL.h" #include "SDL3/SDL_gpu.h" #include "SDL3/SDL_keycode.h" + import sdl_wrapper; // the vertex input layout struct Vertex { float x, y, z; // vec3 position - float r, g, b, a; /** - * @brief Get a pointer to the vertex position's first component. - * - * @return float* Pointer to the `x` member; can be used to access the contiguous position - * components `(x, y, z)`. - */ - auto position() { return &x; } + 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 @@ -35,10 +37,14 @@ struct CameraUniform class UserApp : public sopho::App { - std::shared_ptr gpu_wrapper{std::make_shared()}; - sopho::BufferWrapper vertex_buffer{gpu_wrapper->create_buffer(SDL_GPU_BUFFERUSAGE_VERTEX, sizeof(vertices))}; - sopho::PipelineWrapper pipeline_wrapper{gpu_wrapper->create_pipeline()}; - + // 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)}; + + // camera state float yaw = 0.0f; float pitch = 0.0f; @@ -68,6 +74,7 @@ void main() gl_Position = uView * vec4(a_position, 1.0f); v_color = a_color; })WSQ"; + std::string fragment_source = R"WSQ(#version 460 @@ -79,80 +86,131 @@ void main() FragColor = v_color; })WSQ"; +public: /** * @brief Initialize application resources, GPU pipeline, vertex data, and Dear ImGui. * - * Configures the graphics pipeline and vertex input, uploads the initial vertex buffer contents, - * initializes the camera uniform to the identity matrix, and sets up Dear ImGui (context, style/DPI - * scaling, and SDL3/SDLGPU backends). - * - * @return SDL_AppResult `SDL_APP_CONTINUE` to enter the main loop, `SDL_APP_SUCCESS` to request immediate - * termination. + * 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. */ - virtual SDL_AppResult init(int argc, char** argv) override + SDL_AppResult init(int argc, char** argv) override { + // 1. Create GPU wrapper (device + window + claim), monadic style. + auto gpu_result = sopho::GpuWrapper::create(); + if (!gpu_result) + { + SDL_LogError(SDL_LOG_CATEGORY_ERROR, "Failed to create GpuWrapper, error = %d", + static_cast(gpu_result.error())); + return SDL_APP_FAILURE; + } + 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(); + 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(); }); - pipeline_wrapper.set_vertex_shader(vertex_source); - pipeline_wrapper.set_fragment_shader(fragment_source); - pipeline_wrapper.submit(); + if (!pipeline_init) + { + SDL_LogError(SDL_LOG_CATEGORY_RENDER, "Failed to initialize pipeline, error = %d", + static_cast(pipeline_init.error())); + return SDL_APP_FAILURE; + } - vertex_buffer.upload(&vertices, sizeof(vertices), 0); + // 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); }); + if (!upload_result) { - // identity - cam.m[0] = cam.m[5] = cam.m[10] = cam.m[15] = 1.0F; - cam.m[1] = cam.m[2] = cam.m[3] = 0.0F; - cam.m[4] = cam.m[6] = cam.m[7] = 0.0F; - cam.m[8] = cam.m[9] = cam.m[11] = 0.0F; - cam.m[12] = cam.m[13] = cam.m[14] = 0.0F; + SDL_LogError(SDL_LOG_CATEGORY_GPU, "Failed to upload initial vertex data, error = %d", + static_cast(upload_result.error())); + return SDL_APP_FAILURE; } + // 6. Initialize camera matrix to identity. + { + cam.m.fill(0.0F); + cam.m[0] = 1.0F; + cam.m[5] = 1.0F; + cam.m[10] = 1.0F; + cam.m[15] = 1.0F; + } - // Setup Dear ImGui context + // 7. Setup Dear ImGui context. IMGUI_CHECKVERSION(); ImGui::CreateContext(); ImGuiIO& io = ImGui::GetIO(); (void)io; - io.ConfigFlags |= ImGuiConfigFlags_NavEnableKeyboard; // Enable Keyboard Controls - io.ConfigFlags |= ImGuiConfigFlags_NavEnableGamepad; // Enable Gamepad Controls + io.ConfigFlags |= ImGuiConfigFlags_NavEnableKeyboard; + io.ConfigFlags |= ImGuiConfigFlags_NavEnableGamepad; - // Setup Dear ImGui style ImGui::StyleColorsDark(); - // ImGui::StyleColorsLight(); float main_scale = SDL_GetDisplayContentScale(SDL_GetPrimaryDisplay()); - // Setup scaling + ImGuiStyle& style = ImGui::GetStyle(); style.ScaleAllSizes(main_scale); - // Bake a fixed style scale. (until we have a solution for dynamic style - // scaling, changing this requires resetting Style + calling this again) style.FontScaleDpi = main_scale; - // Set initial font scale. (using io.ConfigDpiScaleFonts=true makes this - // unnecessary. We leave both here for documentation purpose) - - // Setup Platform/Renderer backends - ImGui_ImplSDL3_InitForSDLGPU(gpu_wrapper->acquire_window()); - ImGui_ImplSDLGPU3_InitInfo init_info = {}; - init_info.Device = gpu_wrapper->data(); - init_info.ColorTargetFormat = - SDL_GetGPUSwapchainTextureFormat(gpu_wrapper->data(), gpu_wrapper->acquire_window()); - init_info.MSAASamples = SDL_GPU_SAMPLECOUNT_1; // Only used in multi-viewports mode. - init_info.SwapchainComposition = SDL_GPU_SWAPCHAINCOMPOSITION_SDR; // Only used in multi-viewports mode. + + // 8. Initialize ImGui SDL3 backend. + if (SDL_Window* window = m_gpu->window()) + { + ImGui_ImplSDL3_InitForSDLGPU(window); + } + else + { + SDL_LogError(SDL_LOG_CATEGORY_ERROR, + "GpuWrapper::window() returned null; ImGui SDL3 backend not initialized"); + return SDL_APP_FAILURE; + } + + // 9. Initialize ImGui SDLGPU backend. + auto format_result = m_gpu->get_texture_format(); + if (!format_result) + { + SDL_LogError(SDL_LOG_CATEGORY_GPU, "Failed to get swapchain texture format, error = %d", + static_cast(format_result.error())); + return SDL_APP_FAILURE; + } + + ImGui_ImplSDLGPU3_InitInfo init_info{}; + init_info.Device = m_gpu->device(); + init_info.ColorTargetFormat = format_result.value(); + init_info.MSAASamples = SDL_GPU_SAMPLECOUNT_1; + init_info.SwapchainComposition = SDL_GPU_SWAPCHAINCOMPOSITION_SDR; init_info.PresentMode = SDL_GPU_PRESENTMODE_VSYNC; + ImGui_ImplSDLGPU3_Init(&init_info); return SDL_APP_CONTINUE; } /** - * @brief Advance the UI frame, present interactive editors for triangle vertices and shader source, and apply - * edits. - * - * Presents a NodeEditor with draggable 3D position editors for each vertex and a SourceEditor with a multiline - * shader text editor. If a vertex position is modified, the vertex buffer is updated with the new vertex data. - * If the shader source is modified, the pipeline's vertex shader source is updated. - * - * @return SDL_AppResult SDL_APP_CONTINUE to indicate the application should continue running. + * @brief Advance the UI frame, present editors for triangle vertices and shader sources. */ SDL_AppResult tick() { @@ -162,119 +220,175 @@ void main() ImGui::ShowDemoWindow(); + ImGui::Begin("Editor"); + static int current = 0; + std::array items = {"Node", "Vertex", "Fragment"}; + ImGui::Combo("##Object", ¤t, items.data(), static_cast(items.size())); + + switch (current) { - ImGui::Begin("Editor"); - static int current = 0; - std::array items = {"Node", "Vertex", "Fragment"}; - ImGui::Combo("##Object", ¤t, items.data(), items.size()); - switch (current) + case 0: // Vertex positions { - case 0: + 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; + + if (changed) { - auto change = ImGui::DragFloat3("##node1", vertices[0].position(), 0.01f, -1.f, 1.f); - change = ImGui::DragFloat3("##node2", vertices[1].position(), 0.01f, -1.f, 1.f) || change; - change = ImGui::DragFloat3("##node3", vertices[2].position(), 0.01f, -1.f, 1.f) || change; - if (change) + auto upload_result = m_vertex_buffer.and_then( + [&](auto& vertex_buffer) + { + return vertex_buffer.upload(vertices.data(), static_cast(sizeof(vertices)), + 0); + }); + if (!upload_result) { - // TODO: shouldn't upload in tick, we should delay this into draw function. - vertex_buffer.upload(&vertices, sizeof(vertices), 0); + SDL_LogError(SDL_LOG_CATEGORY_GPU, "Failed to upload vertex buffer in tick(), error = %d", + static_cast(upload_result.error())); } } - break; - case 1: + } + break; + + case 1: // Vertex shader editor + { + auto line_count = std::count(vertex_source.begin(), vertex_source.end(), '\n'); + ImVec2 size(ImGui::GetContentRegionAvail().x, + std::min(ImGui::GetTextLineHeight() * (line_count + 3), ImGui::GetContentRegionAvail().y)); + + if (ImGui::InputTextMultiline("##vertex editor", &vertex_source, size, + ImGuiInputTextFlags_AllowTabInput)) { - auto line_count = std::count(vertex_source.begin(), vertex_source.end(), '\n'); - ImVec2 size = ImVec2( - ImGui::GetContentRegionAvail().x, - std::min(ImGui::GetTextLineHeight() * (line_count + 3), ImGui::GetContentRegionAvail().y)); - 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); }); + if (!result) { - pipeline_wrapper.set_vertex_shader(vertex_source); + SDL_LogError(SDL_LOG_CATEGORY_RENDER, "Failed to set vertex shader from editor, error = %d", + static_cast(result.error())); } } - break; - case 2: + } + break; + + case 2: // Fragment shader editor + { + auto line_count = std::count(fragment_source.begin(), fragment_source.end(), '\n'); + ImVec2 size(ImGui::GetContentRegionAvail().x, + std::min(ImGui::GetTextLineHeight() * (line_count + 3), ImGui::GetContentRegionAvail().y)); + + if (ImGui::InputTextMultiline("##fragment editor", &fragment_source, size, + ImGuiInputTextFlags_AllowTabInput)) { - auto line_count = std::count(fragment_source.begin(), fragment_source.end(), '\n'); - ImVec2 size = ImVec2( - ImGui::GetContentRegionAvail().x, - std::min(ImGui::GetTextLineHeight() * (line_count + 3), ImGui::GetContentRegionAvail().y)); - 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); }); + if (!result) { - pipeline_wrapper.set_fragment_shader(fragment_source); + SDL_LogError(SDL_LOG_CATEGORY_RENDER, "Failed to set fragment shader from editor, error = %d", + static_cast(result.error())); } } - break; - default: - break; } - ImGui::End(); + break; + + default: + break; } + ImGui::End(); ImGui::EndFrame(); return SDL_APP_CONTINUE; } /** - * @brief Render the application's triangle and ImGui UI to the GPU and present the current swapchain frame. - * - * Records and submits GPU commands for drawing the triangle, uploads the per-frame camera uniform, - * renders ImGui draw data into the same render pass, and presents the swapchain texture. - * If no swapchain texture is available, the function still submits any recorded command buffer and continues. - * - * @return SDL_AppResult `SDL_APP_CONTINUE` to continue the application main loop. + * @brief Render the triangle and ImGui UI to the GPU and present the swapchain frame. */ SDL_AppResult draw() { ImGui::Render(); ImDrawData* draw_data = ImGui::GetDrawData(); - pipeline_wrapper.submit(); + // Rebuild pipeline if needed. + auto pipeline_submit = + m_pipeline_wrapper.and_then([](auto& pipeline_wrapper) { return pipeline_wrapper.submit(); }); + if (!pipeline_submit) + { + SDL_LogError(SDL_LOG_CATEGORY_GPU, "Pipeline submit failed, error = %d", + static_cast(pipeline_submit.error())); + // Upload pipeline failed, no need to draw. + return SDL_APP_CONTINUE; + } + + SDL_GPUDevice* device = m_gpu->device(); + if (!device) + { + SDL_LogError(SDL_LOG_CATEGORY_GPU, "GpuWrapper::device() returned null in draw()"); + return SDL_APP_CONTINUE; + } + + SDL_GPUCommandBuffer* commandBuffer = SDL_AcquireGPUCommandBuffer(device); + if (!commandBuffer) + { + SDL_LogError(SDL_LOG_CATEGORY_GPU, "Failed to acquire GPU command buffer"); + return SDL_APP_CONTINUE; + } - // acquire the command buffer - SDL_GPUCommandBuffer* commandBuffer = SDL_AcquireGPUCommandBuffer(gpu_wrapper->data()); + SDL_GPUTexture* swapchainTexture = nullptr; + Uint32 width = 0, height = 0; - // get the swapchain texture - SDL_GPUTexture* swapchainTexture; - Uint32 width, height; - SDL_WaitAndAcquireGPUSwapchainTexture(commandBuffer, gpu_wrapper->acquire_window(), &swapchainTexture, &width, - &height); + SDL_Window* window = m_gpu->window(); + if (!window) + { + SDL_LogError(SDL_LOG_CATEGORY_GPU, "GpuWrapper::window() returned null in draw()"); + SDL_SubmitGPUCommandBuffer(commandBuffer); + return SDL_APP_CONTINUE; + } - // end the frame early if a swapchain texture is not available - if (swapchainTexture == NULL) + if (!SDL_WaitAndAcquireGPUSwapchainTexture(commandBuffer, window, &swapchainTexture, &width, &height)) { - // you must always submit the command buffer + SDL_LogError(SDL_LOG_CATEGORY_GPU, "Failed to acquire swapchain texture: %s", SDL_GetError()); + SDL_SubmitGPUCommandBuffer(commandBuffer); + return SDL_APP_CONTINUE; + } + + if (swapchainTexture == nullptr) + { + // You must always submit the command buffer, even if no texture is available. SDL_SubmitGPUCommandBuffer(commandBuffer); return SDL_APP_CONTINUE; } ImGui_ImplSDLGPU3_PrepareDrawData(draw_data, commandBuffer); - // create the color target + // Create the color target. SDL_GPUColorTargetInfo colorTargetInfo{}; colorTargetInfo.clear_color = {240 / 255.0F, 240 / 255.0F, 240 / 255.0F, 255 / 255.0F}; colorTargetInfo.load_op = SDL_GPU_LOADOP_CLEAR; colorTargetInfo.store_op = SDL_GPU_STOREOP_STORE; colorTargetInfo.texture = swapchainTexture; - // begin a render pass - SDL_GPURenderPass* renderPass = SDL_BeginGPURenderPass(commandBuffer, &colorTargetInfo, 1, NULL); + SDL_GPURenderPass* renderPass = SDL_BeginGPURenderPass(commandBuffer, &colorTargetInfo, 1, nullptr); - // draw calls go here - SDL_BindGPUGraphicsPipeline(renderPass, pipeline_wrapper.data()); + // Bind pipeline if available. + m_pipeline_wrapper.and_then( + [&](auto& pipeline_wrapper) -> std::expected + { + SDL_BindGPUGraphicsPipeline(renderPass, pipeline_wrapper.data()); + return std::monostate{}; + }); + // Compute camera matrix and upload as a vertex uniform. { - float cy = std::cos(yaw); float sy = std::sin(yaw); float cp = std::cos(pitch); float sp = std::sin(pitch); - std::array Ry = {cy, 0.0F, sy, 0.0F, 0.0F, 1.0F, 0.0F, 0.0F, -sy, 0.0F, cy, 0.0F, 0.0F, 0.0F, 0.0F, 1.0F}; + std::array Ry = {cy, 0.0F, sy, 0.0F, 0.0F, 1.0F, 0.0F, 0.0F, + -sy, 0.0F, cy, 0.0F, 0.0F, 0.0F, 0.0F, 1.0F}; - std::array Rx = {1.0F, 0.0F, 0.0F, 0.0F, 0.0F, cp, sp, 0.0F, 0.0F, -sp, cp, 0.0F, 0.0F, 0.0F, 0.0F, 1.0F}; + std::array Rx = {1.0F, 0.0F, 0.0F, 0.0F, 0.0F, cp, sp, 0.0F, + 0.0F, -sp, cp, 0.0F, 0.0F, 0.0F, 0.0F, 1.0F}; auto mulMat4 = [](const float* A, const float* B, float* C) { @@ -290,39 +404,33 @@ void main() // uView = Rx * Ry mulMat4(Rx.data(), Ry.data(), cam.m.data()); - SDL_PushGPUVertexUniformData(commandBuffer, 0, cam.m.data(), sizeof(cam.m)); + SDL_PushGPUVertexUniformData(commandBuffer, 0, cam.m.data(), static_cast(sizeof(cam.m))); } - // bind the vertex buffer - SDL_GPUBufferBinding bufferBindings[1]; - bufferBindings[0].buffer = vertex_buffer.data(); // index 0 is slot 0 in this example - bufferBindings[0].offset = 0; // start from the first byte + // 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); // bind one buffer starting from slot 0 + SDL_BindGPUVertexBuffers(renderPass, 0, bufferBindings, 1); + return std::monostate{}; + }); SDL_DrawGPUPrimitives(renderPass, 3, 1, 0, 0); ImGui_ImplSDLGPU3_RenderDrawData(draw_data, commandBuffer, renderPass); - // end the render pass - SDL_EndGPURenderPass(renderPass); - // submit the command buffer + SDL_EndGPURenderPass(renderPass); SDL_SubmitGPUCommandBuffer(commandBuffer); return SDL_APP_CONTINUE; } - /** - * @brief Advance the application one iteration by performing per-frame updates and rendering. - * - * Performs per-frame UI and state updates; if those updates indicate continuation, executes rendering and submits - * GPU work for presentation. - * - * @return `SDL_APP_CONTINUE` to continue the main loop, or another `SDL_AppResult` to terminate. - */ - virtual SDL_AppResult iterate() override + SDL_AppResult iterate() override { - auto result = tick(); if (result == SDL_APP_CONTINUE) { @@ -331,13 +439,7 @@ void main() return result; } - /** - * @brief Handle an SDL event by forwarding it to ImGui and handling window-close requests. - * - * @param event Pointer to the SDL event to process. - * @return SDL_AppResult `SDL_APP_SUCCESS` when a window close was requested, `SDL_APP_CONTINUE` otherwise. - */ - virtual SDL_AppResult event(SDL_Event* event) override + SDL_AppResult event(SDL_Event* event) override { ImGui_ImplSDL3_ProcessEvent(event); if (event->type == SDL_EVENT_KEY_DOWN) @@ -346,11 +448,11 @@ void main() { case SDLK_UP: pitch += 0.1F; - pitch = std::clamp(pitch, -std::numbers::pi / 2, std::numbers::pi / 2); + pitch = std::clamp(pitch, -std::numbers::pi_v / 2, +std::numbers::pi_v / 2); break; case SDLK_DOWN: pitch -= 0.1F; - pitch = std::clamp(pitch, -std::numbers::pi / 2, std::numbers::pi / 2); + pitch = std::clamp(pitch, -std::numbers::pi_v / 2, +std::numbers::pi_v / 2); break; case SDLK_LEFT: yaw += 0.1F; @@ -362,7 +464,7 @@ void main() break; } } - // close the window on request + if (event->type == SDL_EVENT_WINDOW_CLOSE_REQUESTED) { return SDL_APP_SUCCESS; @@ -371,17 +473,9 @@ void main() return SDL_APP_CONTINUE; } - /** - * @brief Clean up UI and GPU resources and close the application window. - * - * Shuts down ImGui SDL3 and SDLGPU backends, destroys the ImGui context, - * releases the application's association with the GPU device for the window, - * and destroys the SDL window. - * - * @param result Application exit result code provided by the SDL app framework. - */ - virtual void quit(SDL_AppResult result) override + void quit(SDL_AppResult result) override { + (void)result; ImGui_ImplSDL3_Shutdown(); ImGui_ImplSDLGPU3_Shutdown(); ImGui::DestroyContext(); diff --git a/sdl_wrapper/sdl_callback_implement.cpp b/sdl_wrapper/sdl_callback_implement.cpp index 150173f..cdef9f2 100644 --- a/sdl_wrapper/sdl_callback_implement.cpp +++ b/sdl_wrapper/sdl_callback_implement.cpp @@ -18,7 +18,11 @@ extern sopho::App* create_app(int argc, char** argv); */ SDL_AppResult SDL_AppInit(void** appstate, int argc, char** argv) { - SDL_Init(SDL_INIT_VIDEO); + if (!SDL_Init(SDL_INIT_VIDEO)) + { + SDL_LogError(SDL_LOG_CATEGORY_GPU, "%s:%d SDL_INIT_VIDEO:%s", __FILE__, __LINE__, SDL_GetError()); + return SDL_APP_FAILURE; + } auto app = create_app(argc, argv); if (!app) return SDL_APP_FAILURE; diff --git a/sdl_wrapper/sdl_wrapper.buffer.cpp b/sdl_wrapper/sdl_wrapper.buffer.cpp index 38e3cb1..7d2b61c 100644 --- a/sdl_wrapper/sdl_wrapper.buffer.cpp +++ b/sdl_wrapper/sdl_wrapper.buffer.cpp @@ -1,9 +1,14 @@ -// +// sdl_wrapper.buffer.cpp // Created by sophomore on 11/12/25. // module; +#include +#include #include +#include + #include "SDL3/SDL_gpu.h" +#include "SDL3/SDL_log.h" #include "SDL3/SDL_stdinc.h" module sdl_wrapper; import :buffer; @@ -11,59 +16,102 @@ import :gpu; namespace sopho { - /** - * @brief Releases GPU resources owned by this BufferWrapper. - * - * Releases the GPU vertex buffer and, if present, the transfer (staging) buffer, - * then clears the corresponding handles and resets the transfer buffer size. - * - * @note If the associated GPU has already been destroyed, releasing these resources - * may have no effect or may be too late to perform a proper cleanup. - */ - - BufferWrapper::~BufferWrapper() + BufferWrapper::~BufferWrapper() noexcept { - SDL_ReleaseGPUBuffer(m_gpu->data(), m_vertex_buffer); - m_vertex_buffer = nullptr; + if (!m_gpu) + { + return; + } + + // Release vertex buffer + if (m_vertex_buffer) + { + m_gpu->release_buffer(m_vertex_buffer); + m_vertex_buffer = nullptr; + } + + // Release transfer buffer if (m_transfer_buffer) { - SDL_ReleaseGPUTransferBuffer(m_gpu->data(), m_transfer_buffer); + SDL_ReleaseGPUTransferBuffer(m_gpu->device(), m_transfer_buffer); m_transfer_buffer = nullptr; m_transfer_buffer_size = 0; } } - /** - * @brief Uploads a block of data into the wrapped GPU vertex buffer at the specified byte offset. - * - * Reallocates the internal staging (transfer) buffer if its capacity is less than the requested size, - * copies p_size bytes from p_data into the staging buffer, and enqueues a GPU copy pass that writes the data - * into the vertex buffer at p_offset. The GPU command buffer is submitted immediately. - * - * @param p_data Pointer to the source data to upload. - * @param p_size Size in bytes of the data to upload. - * @param p_offset Byte offset within the vertex buffer where the data will be written. - */ - void BufferWrapper::upload(void* p_data, uint32_t p_size, uint32_t p_offset) + [[nodiscard]] std::expected + BufferWrapper::upload(const void* src_data, std::uint32_t size, std::uint32_t offset) { - if (p_size > m_transfer_buffer_size) + // 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* 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(m_gpu->data(), m_transfer_buffer); + 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); } - SDL_GPUTransferBufferCreateInfo transfer_info{SDL_GPU_TRANSFERBUFFERUSAGE_UPLOAD, p_size, 0}; - m_transfer_buffer = SDL_CreateGPUTransferBuffer(m_gpu->data(), &transfer_info); + m_transfer_buffer_size = transfer_info.size; } - auto data = SDL_MapGPUTransferBuffer(m_gpu->data(), m_transfer_buffer, false); - SDL_memcpy(data, p_data, p_size); - SDL_UnmapGPUTransferBuffer(m_gpu->data(), m_transfer_buffer); + // 2. Map the transfer buffer and copy data into it. + void* dst = SDL_MapGPUTransferBuffer(device, m_transfer_buffer, false); + if (!dst) + { + SDL_LogError(SDL_LOG_CATEGORY_GPU, "%s:%d failed to map transfer buffer: %s", __FILE__, __LINE__, + SDL_GetError()); + + return std::unexpected(GpuError::MAP_TRANSFER_BUFFER_FAILED); + } - // TODO: Delay submit command in collect - auto command_buffer = SDL_AcquireGPUCommandBuffer(m_gpu->data()); - auto copy_pass = SDL_BeginGPUCopyPass(command_buffer); + SDL_memcpy(dst, src_data, size); + SDL_UnmapGPUTransferBuffer(device, m_transfer_buffer); + + // 3. Acquire a command buffer and enqueue the copy pass. + auto* command_buffer = SDL_AcquireGPUCommandBuffer(device); + if (!command_buffer) + { + SDL_LogError(SDL_LOG_CATEGORY_GPU, "%s:%d failed to acquire GPU command buffer: %s", __FILE__, __LINE__, + SDL_GetError()); + + return std::unexpected(GpuError::ACQUIRE_COMMAND_BUFFER_FAILED); + } + + auto* copy_pass = SDL_BeginGPUCopyPass(command_buffer); + + if (!copy_pass) + { + SDL_LogError(SDL_LOG_CATEGORY_GPU, "%s:%d failed to begin GPU copy pass: %s", __FILE__, __LINE__, + SDL_GetError()); + SDL_SubmitGPUCommandBuffer(command_buffer); + return std::unexpected(GpuError::BEGIN_COPY_PASS_FAILED); + } SDL_GPUTransferBufferLocation location{}; location.transfer_buffer = m_transfer_buffer; @@ -71,12 +119,19 @@ namespace sopho SDL_GPUBufferRegion region{}; region.buffer = m_vertex_buffer; - region.size = p_size; - region.offset = p_offset; + region.size = size; + region.offset = offset; SDL_UploadToGPUBuffer(copy_pass, &location, ®ion, false); SDL_EndGPUCopyPass(copy_pass); - SDL_SubmitGPUCommandBuffer(command_buffer); + if (!SDL_SubmitGPUCommandBuffer(command_buffer)) + { + SDL_LogError(SDL_LOG_CATEGORY_GPU, "%s:%d %s", __FILE__, __LINE__, SDL_GetError()); + return std::unexpected(GpuError::SUBMIT_COMMAND_FAILED); + } + + return std::monostate{}; } + } // namespace sopho diff --git a/sdl_wrapper/sdl_wrapper.buffer.ixx b/sdl_wrapper/sdl_wrapper.buffer.ixx index 163d9d6..22daa4b 100644 --- a/sdl_wrapper/sdl_wrapper.buffer.ixx +++ b/sdl_wrapper/sdl_wrapper.buffer.ixx @@ -1,9 +1,15 @@ -// +// sdl_wrapper.buffer.ixx // Created by sophomore on 11/8/25. // + module; + +#include #include +#include + #include "SDL3/SDL_gpu.h" + export module sdl_wrapper:buffer; import :decl; @@ -11,22 +17,41 @@ export namespace sopho { class BufferWrapper { - std::shared_ptr m_gpu{}; - SDL_GPUBuffer* m_vertex_buffer{}; - SDL_GPUTransferBuffer* m_transfer_buffer{}; - uint32_t m_transfer_buffer_size{}; + std::shared_ptr m_gpu{}; // Owns the device lifetime + SDL_GPUBuffer* m_vertex_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 - BufferWrapper(std::shared_ptr<::sopho::GpuWrapper> p_gpu, SDL_GPUBuffer* p_buffer) : - m_gpu(p_gpu), m_vertex_buffer(p_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) { } public: - void upload(void* p_data, uint32_t p_size, uint32_t p_offset); + BufferWrapper(const BufferWrapper&) = delete; + BufferWrapper& operator=(const BufferWrapper&) = delete; + 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); + + /// Returns the underlying SDL_GPUBuffer pointer. + [[nodiscard]] SDL_GPUBuffer* data() const noexcept { return m_vertex_buffer; } - auto data() { return m_vertex_buffer; } + ~BufferWrapper() noexcept; - ~BufferWrapper(); - friend GpuWrapper; + friend class GpuWrapper; }; } // namespace sopho diff --git a/sdl_wrapper/sdl_wrapper.decl.ixx b/sdl_wrapper/sdl_wrapper.decl.ixx index 0ac8bce..f2ce808 100644 --- a/sdl_wrapper/sdl_wrapper.decl.ixx +++ b/sdl_wrapper/sdl_wrapper.decl.ixx @@ -6,6 +6,27 @@ export module sdl_wrapper:decl; export namespace sopho { + enum class GpuError + { + UNINITIALIZED, + CREATE_DEVICE_FAILED, + CREATE_WINDOW_FAILED, + CLAIM_WINDOW_FAILED, + CREATE_BUFFER_FAILED, + CREATE_TRANSFER_BUFFER_FAILED, + CREATE_SHADER_FAILED, + CREATE_PIPELINE_FAILED, + GET_TEXTUREFORMAT_FAILED, + BUFFER_OVERFLOW, + MAP_TRANSFER_BUFFER_FAILED, + ACQUIRE_COMMAND_BUFFER_FAILED, + SUBMIT_COMMAND_FAILED, + BEGIN_COPY_PASS_FAILED, + COMPILE_VERTEX_SHADER_FAILED, + COMPILE_FRAGMENT_SHADER_FAILED, + + }; + class App; class GpuWrapper; class BufferWrapper; diff --git a/sdl_wrapper/sdl_wrapper.gpu.cpp b/sdl_wrapper/sdl_wrapper.gpu.cpp index b759408..7bdd5d2 100644 --- a/sdl_wrapper/sdl_wrapper.gpu.cpp +++ b/sdl_wrapper/sdl_wrapper.gpu.cpp @@ -1,19 +1,30 @@ -// +// sdl_wrapper.gpu.cpp // Created by wsqsy on 11/14/2025. // module; -#include +#include +#include "SDL3/SDL_gpu.h" +#include "SDL3/SDL_log.h" module sdl_wrapper; import :gpu; import :pipeline; namespace sopho { - BufferWrapper GpuWrapper::create_buffer(SDL_GPUBufferUsageFlags flag, uint32_t size) + std::expected GpuWrapper::create_buffer(SDL_GPUBufferUsageFlags flag, uint32_t size) { - SDL_GPUBufferCreateInfo create_info{flag, size}; - auto buffer = SDL_CreateGPUBuffer(m_device, &create_info); - BufferWrapper result(shared_from_this(), buffer); - return result; + SDL_GPUBufferCreateInfo create_info{.usage = flag, .size = size}; + auto buffer = SDL_CreateGPUBuffer(device(), &create_info); + if (!buffer) + { + SDL_LogError(SDL_LOG_CATEGORY_GPU, "%s:%d %s", __FILE__, __LINE__, SDL_GetError()); + return std::unexpected(GpuError::CREATE_BUFFER_FAILED); + } + return BufferWrapper{shared_from_this(), buffer, size}; + } + std::expected GpuWrapper::create_pipeline_wrapper() + { + // Query texture format, then construct PipelineWrapper + return get_texture_format().transform([self = shared_from_this()](SDL_GPUTextureFormat format) + { return PipelineWrapper{self, format}; }); } - PipelineWrapper GpuWrapper::create_pipeline() { return PipelineWrapper{shared_from_this()}; } } // namespace sopho diff --git a/sdl_wrapper/sdl_wrapper.gpu.ixx b/sdl_wrapper/sdl_wrapper.gpu.ixx index 23592ca..e8e7137 100644 --- a/sdl_wrapper/sdl_wrapper.gpu.ixx +++ b/sdl_wrapper/sdl_wrapper.gpu.ixx @@ -1,90 +1,288 @@ -// +// sdl_wrapper.gpu.ixx // Created by sophomore on 11/12/25. // module; +#include #include + #include "SDL3/SDL_gpu.h" #include "SDL3/SDL_log.h" #include "SDL3/SDL_video.h" #include "shaderc/shaderc.hpp" export module sdl_wrapper:gpu; +import :decl; import :buffer; import :pipeline; export namespace sopho { - class GpuWrapper : public std::enable_shared_from_this + + struct DeviceHandle { - SDL_GPUDevice* m_device{}; + SDL_GPUDevice* raw{}; - // TODO: Consider multi window situation - SDL_Window* m_window{}; + explicit DeviceHandle(SDL_GPUDevice* d) noexcept : raw(d) {} - public: - GpuWrapper() : m_device(SDL_CreateGPUDevice(SDL_GPU_SHADERFORMAT_SPIRV, true, nullptr)) + DeviceHandle(DeviceHandle&& other) noexcept : raw(other.raw) { other.raw = nullptr; } + DeviceHandle& operator=(DeviceHandle&& other) noexcept { - if (m_device == nullptr) + if (this != &other) { - SDL_LogError(SDL_LOG_CATEGORY_GPU, "%s:%d %s", __FILE__, __LINE__, SDL_GetError()); + reset(); + raw = other.raw; + other.raw = nullptr; } + return *this; } - ~GpuWrapper() + DeviceHandle(const DeviceHandle&) = delete; + DeviceHandle& operator=(const DeviceHandle&) = delete; + + ~DeviceHandle() { reset(); } + + void reset() noexcept { - if (m_window) + if (raw) { - SDL_ReleaseWindowFromGPUDevice(m_device, m_window); - SDL_DestroyWindow(m_window); + SDL_DestroyGPUDevice(raw); + raw = nullptr; } - if (m_device) + } + + [[nodiscard]] bool valid() const noexcept { return raw != nullptr; } + }; + + struct WindowHandle + { + SDL_Window* raw{}; + + WindowHandle() = default; + + explicit WindowHandle(SDL_Window* w) noexcept : raw(w) {} + + WindowHandle(const WindowHandle&) = delete; + WindowHandle& operator=(const WindowHandle&) = delete; + + WindowHandle(WindowHandle&& other) noexcept : raw(other.raw) { other.raw = nullptr; } + + WindowHandle& operator=(WindowHandle&& other) noexcept + { + if (this != &other) { - SDL_DestroyGPUDevice(m_device); + reset(); + raw = other.raw; + other.raw = nullptr; } - m_device = nullptr; + return *this; } - auto data() { return m_device; } + ~WindowHandle() noexcept { reset(); } - BufferWrapper create_buffer(SDL_GPUBufferUsageFlags flag, uint32_t size); + void reset() noexcept + { + if (raw) + { + SDL_DestroyWindow(raw); + raw = nullptr; + } + } - PipelineWrapper create_pipeline(); + [[nodiscard]] bool valid() const noexcept { return raw != nullptr; } + }; + + struct ClaimedWindow + { + SDL_GPUDevice* device{}; + SDL_Window* window{}; - auto create_shader(const std::vector& p_shader, SDL_GPUShaderStage p_stage,uint32_t p_num_uniform_buffers) + ClaimedWindow() = default; + + ClaimedWindow(SDL_GPUDevice* d, SDL_Window* w) noexcept : device(d), window(w) {} + + ClaimedWindow(const ClaimedWindow&) = delete; + ClaimedWindow& operator=(const ClaimedWindow&) = delete; + + ClaimedWindow(ClaimedWindow&& other) noexcept : device(other.device), window(other.window) { - SDL_GPUShaderCreateInfo vertexInfo{}; - vertexInfo.code = p_shader.data(); - vertexInfo.code_size = p_shader.size(); - vertexInfo.entrypoint = "main"; - vertexInfo.format = SDL_GPU_SHADERFORMAT_SPIRV; - vertexInfo.stage = p_stage; - vertexInfo.num_samplers = 0; - vertexInfo.num_storage_buffers = 0; - vertexInfo.num_storage_textures = 0; - vertexInfo.num_uniform_buffers = p_num_uniform_buffers; - return SDL_CreateGPUShader(m_device, &vertexInfo); + other.device = nullptr; + other.window = nullptr; } - auto release_shader(SDL_GPUShader* shader) + ClaimedWindow& operator=(ClaimedWindow&& other) noexcept { - if (shader) + if (this != &other) { - SDL_ReleaseGPUShader(m_device, shader); + reset(); + device = other.device; + window = other.window; + other.device = nullptr; + other.window = nullptr; } + return *this; } - auto acquire_window() + ~ClaimedWindow() noexcept { reset(); } + + void reset() noexcept { - if (!m_window) + if (device && window) { - m_window = SDL_CreateWindow("Hello, Triangle!", 960, 540, SDL_WINDOW_RESIZABLE); - SDL_ClaimWindowForGPUDevice(m_device, m_window); + SDL_ReleaseWindowFromGPUDevice(device, window); + device = nullptr; + window = nullptr; } - return m_window; } - auto get_texture_format() + [[nodiscard]] bool valid() const noexcept { return device && window; } + }; + + struct GpuContext + { + DeviceHandle device; + WindowHandle window; + ClaimedWindow claimed; + + GpuContext(DeviceHandle d, WindowHandle w, ClaimedWindow c) noexcept : + device(std::move(d)), window(std::move(w)), claimed(std::move(c)) + { + } + + GpuContext(const GpuContext&) = delete; + GpuContext& operator=(const GpuContext&) = delete; + + GpuContext(GpuContext&&) = default; + GpuContext& operator=(GpuContext&&) = default; + + [[nodiscard]] SDL_GPUDevice* device_raw() const noexcept { return device.raw; } + [[nodiscard]] SDL_Window* window_raw() const noexcept { return window.raw; } + }; + + using GpuContextResult = std::expected; + + inline GpuContextResult create_gpu_context() + { + auto* dev_raw = SDL_CreateGPUDevice(SDL_GPU_SHADERFORMAT_SPIRV, true, nullptr); + if (!dev_raw) + { + SDL_LogError(SDL_LOG_CATEGORY_GPU, "%s:%d %s", __FILE__, __LINE__, SDL_GetError()); + return std::unexpected(GpuError::CREATE_DEVICE_FAILED); + } + DeviceHandle device{dev_raw}; + + auto* win_raw = SDL_CreateWindow("Hello, Triangle!", 960, 540, SDL_WINDOW_RESIZABLE); + if (!win_raw) + { + SDL_LogError(SDL_LOG_CATEGORY_GPU, "%s:%d %s", __FILE__, __LINE__, SDL_GetError()); + return std::unexpected(GpuError::CREATE_WINDOW_FAILED); + } + WindowHandle window{win_raw}; + + if (!SDL_ClaimWindowForGPUDevice(device.raw, window.raw)) { - return SDL_GetGPUSwapchainTextureFormat(m_device, acquire_window()); + SDL_LogError(SDL_LOG_CATEGORY_GPU, "%s:%d %s", __FILE__, __LINE__, SDL_GetError()); + return std::unexpected(GpuError::CLAIM_WINDOW_FAILED); } + ClaimedWindow claimed{device.raw, window.raw}; + return GpuContext{std::move(device), std::move(window), std::move(claimed)}; + } + + class GpuWrapper : public std::enable_shared_from_this + { + + GpuContext m_ctx; + + explicit GpuWrapper(GpuContext ctx) noexcept : m_ctx(std::move(ctx)) {} + + public: + [[nodiscard]] static std::expected, GpuError> create() + { + return create_gpu_context().transform( + [](GpuContext ctx) { return std::shared_ptr(new GpuWrapper(std::move(ctx))); }); + } + + // Release resource in m_ctx + ~GpuWrapper() = default; + + GpuWrapper(const GpuWrapper&) = delete; + GpuWrapper& operator=(const GpuWrapper&) = delete; + + GpuWrapper(GpuWrapper&&) = delete; + GpuWrapper& operator=(GpuWrapper&&) = delete; + + [[nodiscard]] auto device() const { return m_ctx.device.raw; } + [[nodiscard]] auto window() const { return m_ctx.window.raw; } + + [[nodiscard]] std::expected create_buffer(SDL_GPUBufferUsageFlags flag, uint32_t size); + + auto release_buffer(SDL_GPUBuffer* buffer) + { + if (buffer) + { + SDL_ReleaseGPUBuffer(device(), buffer); + } + } + + [[nodiscard]] std::expected create_pipeline_wrapper(); + + std::expected + create_pipeline(const SDL_GPUGraphicsPipelineCreateInfo& create_info) + { + auto pipeline = SDL_CreateGPUGraphicsPipeline(device(), &create_info); + if (!pipeline) + { + SDL_LogError(SDL_LOG_CATEGORY_GPU, "%s:%d %s", __FILE__, __LINE__, SDL_GetError()); + return std::unexpected(GpuError::CREATE_PIPELINE_FAILED); + } + return pipeline; + } + + auto release_pipeline(SDL_GPUGraphicsPipeline* graphics_pipeline) + { + if (graphics_pipeline) + { + SDL_ReleaseGPUGraphicsPipeline(device(), graphics_pipeline); + } + } + + [[nodiscard]] auto create_shader(const std::vector& shader, SDL_GPUShaderStage stage, + uint32_t num_uniform_buffers) -> std::expected + { + SDL_GPUShaderCreateInfo info{}; + info.code = shader.data(); + info.code_size = shader.size(); + info.entrypoint = "main"; + info.format = SDL_GPU_SHADERFORMAT_SPIRV; + info.stage = stage; + info.num_samplers = 0; + info.num_storage_buffers = 0; + info.num_storage_textures = 0; + info.num_uniform_buffers = num_uniform_buffers; + auto s = SDL_CreateGPUShader(device(), &info); + if (!s) + { + SDL_LogError(SDL_LOG_CATEGORY_GPU, "%s:%d %s", __FILE__, __LINE__, SDL_GetError()); + return std::unexpected(GpuError::CREATE_SHADER_FAILED); + } + return s; + } + + auto release_shader(SDL_GPUShader* shader) + { + if (shader) + { + SDL_ReleaseGPUShader(device(), shader); + } + } + + [[nodiscard]] std::expected get_texture_format() const + { + auto format = SDL_GetGPUSwapchainTextureFormat(device(), window()); + if (format == SDL_GPU_TEXTUREFORMAT_INVALID) + { + SDL_LogError(SDL_LOG_CATEGORY_GPU, "%s:%d %s", __FILE__, __LINE__, SDL_GetError()); + return std::unexpected(GpuError::GET_TEXTUREFORMAT_FAILED); + } + return format; + } }; } // namespace sopho diff --git a/sdl_wrapper/sdl_wrapper.pipeline.cpp b/sdl_wrapper/sdl_wrapper.pipeline.cpp index 95fd6e5..623bd56 100644 --- a/sdl_wrapper/sdl_wrapper.pipeline.cpp +++ b/sdl_wrapper/sdl_wrapper.pipeline.cpp @@ -1,162 +1,248 @@ -// +// sdl_wrapper.pipeline.cpp // Created by sophomore on 11/13/25. // module; +#include #include +#include +#include +#include + #include "SDL3/SDL_gpu.h" #include "SDL3/SDL_log.h" #include "shaderc/shaderc.hpp" module sdl_wrapper; import :pipeline; +import :gpu; namespace sopho { - /** - * @brief Constructs a PipelineWrapper and configures default pipeline state for the provided GPU device. - * - * Stores the provided GPU device wrapper for the wrapper's lifetime, configures the shaderc target environment, - * and initializes default vertex input state, primitive type, and color target description used when creating - * graphics pipelines. - * - * @param p_device Shared pointer to the GpuWrapper used to create and release shaders and graphics pipelines. - */ - PipelineWrapper::PipelineWrapper(std::shared_ptr<::sopho::GpuWrapper> p_device) : m_device(p_device) + // Helper: convert shaderc SPIR-V result (words) into a byte buffer. + static std::vector spv_result_to_bytes(const shaderc::SpvCompilationResult& result) { - options.SetTargetEnvironment(shaderc_target_env_vulkan, 0); + // shaderc::SpvCompilationResult is an iterable sequence of uint32_t words. + auto begin = result.cbegin(); + auto end = result.cend(); + const std::size_t word_count = static_cast(end - begin); + + std::vector bytes; + bytes.reserve(word_count * sizeof(std::uint32_t)); + + // Safely read each word and append its bytes. + for (auto it = begin; it != end; ++it) + { + std::uint32_t word = *it; + const auto* ptr = reinterpret_cast(&word); + bytes.insert(bytes.end(), ptr, ptr + sizeof(std::uint32_t)); + } + + return bytes; + } + + PipelineWrapper::PipelineWrapper(std::shared_ptr gpu, SDL_GPUTextureFormat swapchain_format) noexcept : + m_gpu(std::move(gpu)) + { + // 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.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{}; + blend.src_color_blendfactor = SDL_GPU_BLENDFACTOR_SRC_ALPHA; + blend.dst_color_blendfactor = SDL_GPU_BLENDFACTOR_ONE_MINUS_SRC_ALPHA; + blend.color_blend_op = SDL_GPU_BLENDOP_ADD; + blend.src_alpha_blendfactor = SDL_GPU_BLENDFACTOR_SRC_ALPHA; + blend.dst_alpha_blendfactor = SDL_GPU_BLENDFACTOR_ONE_MINUS_SRC_ALPHA; + blend.alpha_blend_op = SDL_GPU_BLENDOP_ADD; + blend.enable_blend = true; + + SDL_GPUColorTargetDescription color_target{}; + color_target.format = swapchain_format; + color_target.blend_state = blend; - m_vertex_buffer_description.emplace_back(0, 28, SDL_GPU_VERTEXINPUTRATE_VERTEX, 0); - m_pipeline_info.vertex_input_state.vertex_buffer_descriptions = m_vertex_buffer_description.data(); - m_pipeline_info.vertex_input_state.num_vertex_buffers = m_vertex_buffer_description.size(); + m_color_target_descriptions.push_back(color_target); - m_vertex_attribute.emplace_back(0, 0, SDL_GPU_VERTEXELEMENTFORMAT_FLOAT3, 0); - m_vertex_attribute.emplace_back(1, 0, SDL_GPU_VERTEXELEMENTFORMAT_FLOAT4, sizeof(float) * 3); - m_pipeline_info.vertex_input_state.vertex_attributes = m_vertex_attribute.data(); - m_pipeline_info.vertex_input_state.num_vertex_attributes = m_vertex_attribute.size(); + // Initialize pipeline create info with default state. + 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()); - m_pipeline_info.primitive_type = SDL_GPU_PRIMITIVETYPE_TRIANGLELIST; + info.primitive_type = SDL_GPU_PRIMITIVETYPE_TRIANGLELIST; - m_color_target_description.emplace_back(m_device->get_texture_format(), - SDL_GPUColorTargetBlendState{SDL_GPU_BLENDFACTOR_SRC_ALPHA, - SDL_GPU_BLENDFACTOR_ONE_MINUS_SRC_ALPHA, - SDL_GPU_BLENDOP_ADD, - SDL_GPU_BLENDFACTOR_SRC_ALPHA, - SDL_GPU_BLENDFACTOR_ONE_MINUS_SRC_ALPHA, - SDL_GPU_BLENDOP_ADD, - {}, - true}); + info.target_info.color_target_descriptions = m_color_target_descriptions.data(); + info.target_info.num_color_targets = static_cast(m_color_target_descriptions.size()); - m_pipeline_info.target_info.color_target_descriptions = m_color_target_description.data(); - m_pipeline_info.target_info.num_color_targets = m_color_target_description.size(); + // Shaders will be filled in later by set_vertex_shader / set_fragment_shader. + info.vertex_shader = nullptr; + info.fragment_shader = nullptr; + m_pipeline_info = info; m_modified = true; } - /** - * @brief Releases any GPU graphics pipeline owned by this wrapper. - * - * If a graphics pipeline is currently held, it is released using the associated - * device and the stored pipeline handle is cleared so the wrapper no longer - * references the pipeline. - */ - PipelineWrapper::~PipelineWrapper() + PipelineWrapper::~PipelineWrapper() noexcept { + if (!m_gpu) + { + return; + } + if (m_graphics_pipeline) { - SDL_ReleaseGPUGraphicsPipeline(m_device->data(), m_graphics_pipeline); + m_gpu->release_pipeline(m_graphics_pipeline); m_graphics_pipeline = nullptr; } - m_device->release_shader(m_vertex_shader); - m_device->release_shader(m_fragment_shader); + + if (m_vertex_shader) + { + m_gpu->release_shader(m_vertex_shader); + m_vertex_shader = nullptr; + } + + if (m_fragment_shader) + { + m_gpu->release_shader(m_fragment_shader); + m_fragment_shader = nullptr; + } } - /** - * @brief Rebuilds the GPU graphics pipeline when the wrapper is marked modified. - * - * If the wrapper's modified flag is set, this clears the flag, attempts to create a new - * graphics pipeline from the stored pipeline info and device, and on success replaces the - * current pipeline (releasing the previous pipeline first). If creation fails, an error - * is logged. - */ - void PipelineWrapper::submit() + + [[nodiscard]] std::expected PipelineWrapper::submit() { - if (m_modified) + if (!m_modified) + { + // Nothing changed, nothing to do. + return std::monostate{}; + } + + // Refresh pointers in case the underlying vectors were reallocated. + m_pipeline_info.vertex_input_state.vertex_buffer_descriptions = m_vertex_buffer_descriptions.data(); + 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.num_vertex_attributes = + static_cast(m_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()); + + // Attempt to create a new graphics pipeline. + auto pipeline_result = m_gpu->create_pipeline(m_pipeline_info); + if (!pipeline_result) + { + SDL_LogError(SDL_LOG_CATEGORY_GPU, "%s:%d failed to create graphics pipeline", __FILE__, __LINE__); + return std::unexpected(pipeline_result.error()); + } + + SDL_GPUGraphicsPipeline* new_pipeline = pipeline_result.value(); + + // Replace previous pipeline if any. + if (m_graphics_pipeline) { - m_modified = false; - m_pipeline_info.vertex_input_state.vertex_buffer_descriptions = m_vertex_buffer_description.data(); - m_pipeline_info.vertex_input_state.vertex_attributes = m_vertex_attribute.data(); - m_pipeline_info.target_info.color_target_descriptions = m_color_target_description.data(); - auto new_graphics_pipeline = SDL_CreateGPUGraphicsPipeline(m_device->data(), &m_pipeline_info); - if (new_graphics_pipeline) - { - if (m_graphics_pipeline) - { - SDL_ReleaseGPUGraphicsPipeline(m_device->data(), m_graphics_pipeline); - } - m_graphics_pipeline = new_graphics_pipeline; - } - else - { - SDL_LogError(SDL_LOG_CATEGORY_GPU, "%s get error:%s", __FUNCTION__, SDL_GetError()); - } + m_gpu->release_pipeline(m_graphics_pipeline); } + + m_graphics_pipeline = new_pipeline; + m_modified = false; + + return std::monostate{}; } - /** - * @brief Compile and install a new vertex shader from GLSL source. - * - * Compiles the provided GLSL vertex shader source to SPIR‑V, replaces any previously installed - * vertex shader on the device with the newly created shader, updates the pipeline's vertex - * shader reference, and marks the pipeline wrapper as modified so the pipeline will be rebuilt. - * - * @param p_source GLSL source code for the vertex shader. - * - * On compilation failure, a compilation error is logged and the previously installed shader is left unchanged. - */ - void PipelineWrapper::set_vertex_shader(const std::string& p_source) + [[nodiscard]] std::expected PipelineWrapper::set_vertex_shader(const std::string& source) { - auto result = compiler.CompileGlslToSpv(p_source, shaderc_glsl_vertex_shader, "vertex.glsl", options); + auto result = m_compiler.CompileGlslToSpv(source, shaderc_glsl_vertex_shader, "vertex.glsl", m_options); if (result.GetCompilationStatus() != shaderc_compilation_status_success) { SDL_LogError(SDL_LOG_CATEGORY_RENDER, "[shaderc] compile error in vertex.glsl: %s", - result.GetErrorMessage().data()); + result.GetErrorMessage().c_str()); + + // You may want to add this to your GpuError enum. + return std::unexpected(GpuError::COMPILE_VERTEX_SHADER_FAILED); } - else + + // Convert SPIR-V words to a byte vector + std::vector code = spv_result_to_bytes(result); + + auto shader_result = m_gpu->create_shader(code, SDL_GPU_SHADERSTAGE_VERTEX, 1); + if (!shader_result) + { + return std::unexpected(shader_result.error()); + } + + SDL_GPUShader* new_shader = shader_result.value(); + + // Release previous shader, if any + if (m_vertex_shader) { - m_device->release_shader(m_vertex_shader); - auto code_size = static_cast(result.cend() - result.cbegin()) * sizeof(uint32_t); - auto ptr = reinterpret_cast(result.cbegin()); - std::vector code{ptr, ptr + code_size}; - m_vertex_shader = m_device->create_shader(code, SDL_GPU_SHADERSTAGE_VERTEX, 1); - m_pipeline_info.vertex_shader = m_vertex_shader; - m_modified = true; + m_gpu->release_shader(m_vertex_shader); } + + m_vertex_shader = new_shader; + m_pipeline_info.vertex_shader = new_shader; + m_modified = true; + + return std::monostate{}; } - /** - * @brief Compile and install a fragment shader from GLSL source. - * - * Compiles the provided GLSL fragment shader source to SPIR‑V; on compilation failure logs the error. - * On success, releases the previously installed fragment shader (if any), uploads the new SPIR‑V bytecode - * to the device as a fragment-stage shader, updates the internal pipeline description to reference it, - * and marks the wrapper as modified so the graphics pipeline will be rebuilt. - * - * @param p_source GLSL source code for the fragment shader. - */ - void PipelineWrapper::set_fragment_shader(const std::string& p_source) + + [[nodiscard]] std::expected + PipelineWrapper::set_fragment_shader(const std::string& source) { - auto result = compiler.CompileGlslToSpv(p_source, shaderc_glsl_fragment_shader, "fragment.glsl", options); + auto result = m_compiler.CompileGlslToSpv(source, shaderc_glsl_fragment_shader, "fragment.glsl", m_options); if (result.GetCompilationStatus() != shaderc_compilation_status_success) { - SDL_LogError(SDL_LOG_CATEGORY_RENDER, "[shaderc] compile error fragment.glsl: %s", - result.GetErrorMessage().data()); + SDL_LogError(SDL_LOG_CATEGORY_RENDER, "[shaderc] compile error in fragment.glsl: %s", + result.GetErrorMessage().c_str()); + + // You may want to add this to your GpuError enum. + return std::unexpected(GpuError::COMPILE_FRAGMENT_SHADER_FAILED); } - else + + // Convert SPIR-V words to a byte vector + std::vector code = spv_result_to_bytes(result); + + auto shader_result = m_gpu->create_shader(code, SDL_GPU_SHADERSTAGE_FRAGMENT, 0); + if (!shader_result) { - m_device->release_shader(m_fragment_shader); - auto code_size = static_cast(result.cend() - result.cbegin()) * sizeof(uint32_t); - auto ptr = reinterpret_cast(result.cbegin()); - std::vector code{ptr, ptr + code_size}; - m_fragment_shader = m_device->create_shader(code, SDL_GPU_SHADERSTAGE_FRAGMENT, 0); - m_pipeline_info.fragment_shader = m_fragment_shader; - m_modified = true; + return std::unexpected(shader_result.error()); } + + SDL_GPUShader* new_shader = shader_result.value(); + + // Release previous shader, if any + if (m_fragment_shader) + { + m_gpu->release_shader(m_fragment_shader); + } + + m_fragment_shader = new_shader; + m_pipeline_info.fragment_shader = new_shader; + m_modified = true; + + return std::monostate{}; } + } // namespace sopho diff --git a/sdl_wrapper/sdl_wrapper.pipeline.ixx b/sdl_wrapper/sdl_wrapper.pipeline.ixx index c410292..f353e2c 100644 --- a/sdl_wrapper/sdl_wrapper.pipeline.ixx +++ b/sdl_wrapper/sdl_wrapper.pipeline.ixx @@ -1,41 +1,71 @@ -// +// sdl_wrapper.pipeline.ixx // Created by sophomore on 11/11/25. // module; +#include #include +#include +#include +#include + #include "SDL3/SDL_gpu.h" #include "shaderc/shaderc.hpp" export module sdl_wrapper:pipeline; -import :decl; +import :decl; // GpuError, forward declarations, etc. + export namespace sopho { class PipelineWrapper { - SDL_GPUGraphicsPipeline* m_graphics_pipeline{}; - std::shared_ptr m_device{}; + std::shared_ptr m_gpu{}; + SDL_GPUGraphicsPipeline* m_graphics_pipeline{}; SDL_GPUShader* m_vertex_shader{}; SDL_GPUShader* m_fragment_shader{}; - std::vector m_vertex_buffer_description{}; - std::vector m_vertex_attribute{}; - std::vector m_color_target_description{}; + + std::vector m_vertex_buffer_descriptions{}; + std::vector m_vertex_attributes{}; + std::vector m_color_target_descriptions{}; SDL_GPUGraphicsPipelineCreateInfo m_pipeline_info{}; - shaderc::Compiler compiler{}; - shaderc::CompileOptions options{}; + shaderc::Compiler m_compiler{}; + shaderc::CompileOptions m_options{}; + + bool m_modified{false}; - bool m_modified = false; + // Internal constructor: assumes texture format is already known and valid. + PipelineWrapper(std::shared_ptr gpu, SDL_GPUTextureFormat swapchain_format) noexcept; - PipelineWrapper(std::shared_ptr p_device); public: - ~PipelineWrapper(); + PipelineWrapper(const PipelineWrapper&) = delete; + PipelineWrapper& operator=(const PipelineWrapper&) = delete; + PipelineWrapper(PipelineWrapper&&) noexcept = default; + PipelineWrapper& operator=(PipelineWrapper&&) = delete; + ~PipelineWrapper() noexcept; + + /// Returns the underlying SDL_GPUGraphicsPipeline*. + [[nodiscard]] SDL_GPUGraphicsPipeline* data() const noexcept { return m_graphics_pipeline; } + + /// Rebuilds the graphics pipeline if any state has been modified. + /// + /// On success, this replaces the previous pipeline (if any) with a newly created one. + /// On failure, an error is propagated to the caller. + [[nodiscard]] std::expected submit(); - auto data() { return m_graphics_pipeline; } + /// Compiles and sets the vertex shader from GLSL source. + /// + /// On success, this replaces the previous vertex shader (if any), updates the + /// pipeline create info, and marks the pipeline as modified. + /// On failure, a compile or creation error is returned. + [[nodiscard]] std::expected set_vertex_shader(const std::string& source); - void submit(); + /// Compiles and sets the fragment shader from GLSL source. + /// + /// On success, this replaces the previous fragment shader (if any), updates the + /// pipeline create info, and marks the pipeline as modified. + /// On failure, a compile or creation error is returned. + [[nodiscard]] std::expected set_fragment_shader(const std::string& source); - void set_vertex_shader(const std::string& p_source); - void set_fragment_shader(const std::string& p_source); - friend GpuWrapper; + friend class GpuWrapper; }; } // namespace sopho