diff --git a/.coderabbit.yaml b/.coderabbit.yaml index 3424534..82f4ff3 100644 --- a/.coderabbit.yaml +++ b/.coderabbit.yaml @@ -22,7 +22,7 @@ reviews: related_prs: true suggested_labels: true labeling_instructions: [] - auto_apply_labels: false + auto_apply_labels: true suggested_reviewers: true auto_assign_reviewers: false in_progress_fortune: true diff --git a/.github/workflows/clang-tidy-review.yml b/.github/workflows/clang-tidy-review.yml new file mode 100644 index 0000000..9e5e26f --- /dev/null +++ b/.github/workflows/clang-tidy-review.yml @@ -0,0 +1,42 @@ +name: clang-tidy-review + +on: + pull_request: + +jobs: + clang-tidy-review: + runs-on: ubuntu-latest + permissions: + contents: read + pull-requests: write + + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Install SDL windowing deps (Linux) + run: | + sudo apt-get update + sudo apt-get install -y \ + build-essential cmake ninja-build pkg-config gcc g++ clang git python3 python3-pip \ + libasound2-dev libjack-jackd2-dev libpulse-dev \ + xorg-dev libx11-dev libxext-dev libxrandr-dev libxcursor-dev libxfixes-dev libxi-dev libxss-dev libxtst-dev \ + libxkbcommon-dev wayland-protocols libwayland-dev \ + libdrm-dev mesa-common-dev mesa-utils + + - name: Configure CMake + run: | + cmake -S ${{ github.workspace }} \ + -B build \ + -G Ninja \ + -DCMAKE_CXX_COMPILER=clang++ \ + -DCMAKE_C_COMPILER=clang \ + -DCMAKE_BUILD_TYPE=Release \ + -DCMAKE_EXPORT_COMPILE_COMMANDS=ON + + - name: Run clang-tidy-review + uses: ZedThree/clang-tidy-review@v0.21.0 + id: review + with: + token: ${{ secrets.GITHUB_TOKEN }} + build_dir: build diff --git a/.github/workflows/cmake-windows.yml b/.github/workflows/cmake-windows.yml index 970e4ab..acb14af 100644 --- a/.github/workflows/cmake-windows.yml +++ b/.github/workflows/cmake-windows.yml @@ -61,7 +61,20 @@ jobs: - name: Build # Build your program with the given configuration. Note that --config is needed because the default Windows generator is a multi-config generator (Visual Studio generator). - run: cmake --build ${{ steps.strings.outputs.build-output-dir }} --config ${{ matrix.build_type }} + run: cmake --build ${{ steps.strings.outputs.build-output-dir }} --config ${{ matrix.build_type }} 1> build.log 2>&1 + shell: pwsh + + - name: Print log tail on failure + if: failure() + run: Get-Content build.log -Tail 200 + shell: pwsh + + - name: Upload full build log + if: always() + uses: actions/upload-artifact@v4 + with: + name: ${{ matrix.os }}-${{ matrix.c_compiler }}-${{ matrix.build_type }}-build-log-${{ github.sha }} + path: build.log - name: Test working-directory: ${{ steps.strings.outputs.build-output-dir }} diff --git a/.github/workflows/gen-pr.yml b/.github/workflows/gen-pr.yml new file mode 100644 index 0000000..468d738 --- /dev/null +++ b/.github/workflows/gen-pr.yml @@ -0,0 +1,33 @@ +name: Generate PR with AI + +on: + workflow_dispatch: + inputs: + issue-number: + description: "Issue number to implement" + required: true + type: number + +jobs: + gen-pr: + runs-on: ubuntu-latest + + permissions: + contents: write + pull-requests: write + issues: read + + env: + GH_TOKEN: ${{ github.token }} + OPENROUTER_API_KEY: ${{ secrets.OPENROUTER_API_KEY }} + + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Generate PR from issue using OpenRouter + uses: WillBooster/gen-pr@v4.1.4 + with: + issue-number: ${{ inputs.issue-number }} + aider-extra-args: "--model openrouter/openrouter/polaris-alpha" + verbose: true diff --git a/CMakeLists.txt b/CMakeLists.txt index 1923319..e1d5ca2 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -4,6 +4,8 @@ set(CMAKE_CXX_STANDARD 23) set(CMAKE_CXX_STANDARD_REQUIRED ON) include(FetchContent) +set(CMAKE_EXPORT_COMPILE_COMMANDS ON) + set(FETCHCONTENT_QUIET OFF CACHE BOOL "" FORCE) set(CMAKE_DOWNLOAD_NO_PROGRESS OFF CACHE BOOL "" FORCE) set(CMAKE_MESSAGE_LOG_LEVEL STATUS CACHE STRING "" FORCE) @@ -95,7 +97,7 @@ target_link_libraries(imgui PUBLIC SDL3::SDL3) add_subdirectory(sdl_wrapper) add_executable(SDL_TEST main.cpp) -target_link_libraries(SDL_TEST PRIVATE shaderc imgui sdl_wrapper) +target_link_libraries(SDL_TEST PRIVATE imgui sdl_wrapper) include(GNUInstallDirs) diff --git a/gen-pr.config.yml b/gen-pr.config.yml new file mode 100644 index 0000000..7e0b7dc --- /dev/null +++ b/gen-pr.config.yml @@ -0,0 +1,12 @@ +# gen-pr.config.yml +planning-model: openrouter/openrouter/polaris-alpha +reasoning-effort: high + +repomix-extra-args: "--compress --remove-empty-lines --include '**/*.cpp' --include '**/*.ixx'" + +coding-tool: aider +aider-extra-args: "--model openrouter/openrouter/polaris-alpha" +verbose: true + +# Run build tests after code generation +test-command: "cmake -S . -B build" diff --git a/main.cpp b/main.cpp index 15323bb..b6d51ad 100644 --- a/main.cpp +++ b/main.cpp @@ -9,7 +9,8 @@ #include "misc/cpp/imgui_stdlib.h" #include - +#include +#include "SDL3/SDL_log.h" import sdl_wrapper; // the vertex input layout @@ -22,11 +23,12 @@ struct Vertex class UserApp : public sopho::App { - std::optional vertexBuffer; + std::shared_ptr gpu_wrapper{std::make_shared()}; + sopho::BufferWrapper vertex_buffer{gpu_wrapper->create_buffer(SDL_GPU_BUFFERUSAGE_VERTEX, sizeof(vertices))}; + std::optional pipeline_wrapper{std::nullopt}; SDL_Window* window{}; - SDL_GPUDevice* device{}; - SDL_GPUGraphicsPipeline* graphicsPipeline{}; + // SDL_GPUGraphicsPipeline* graphicsPipeline{}; // a list of vertices std::array vertices{ @@ -62,89 +64,44 @@ void main() shaderc::CompileOptions options{}; SDL_GPUGraphicsPipelineCreateInfo pipelineInfo{}; - SDL_GPUShader* vertexShader{}; - SDL_GPUShader* fragmentShader{}; SDL_GPUColorTargetDescription colorTargetDescriptions[1]{}; SDL_GPUVertexAttribute vertexAttributes[2]{}; SDL_GPUVertexBufferDescription vertexBufferDesctiptions[1]{}; + /** + * @brief Initialize the application: create the window, configure GPU pipeline and resources, upload initial vertex + * data, and initialize ImGui. + * + * Performs window creation and GPU device claim, configures vertex input and color target state, sets vertex and + * fragment shaders on the pipeline wrapper and submits pipeline creation, uploads initial vertex data to the vertex + * buffer, and initializes Dear ImGui (context, style scaling, and SDL3/SDLGPU backends). + * + * @return SDL_AppResult `SDL_APP_CONTINUE` to enter the main loop, `SDL_APP_SUCCESS` to request immediate + * termination. + */ virtual SDL_AppResult init(int argc, char** argv) override { // create a window window = SDL_CreateWindow("Hello, Triangle!", 960, 540, SDL_WINDOW_RESIZABLE); + gpu_wrapper->set_window(window); - // create the device - device = SDL_CreateGPUDevice(SDL_GPU_SHADERFORMAT_SPIRV, true, NULL); - SDL_ClaimWindowForGPUDevice(device, window); - - options.SetTargetEnvironment(shaderc_target_env_vulkan, 0); - auto result = compiler.CompileGlslToSpv(vertex_source, shaderc_glsl_vertex_shader, "test.glsl", options); - - if (result.GetCompilationStatus() != shaderc_compilation_status_success) - { - std::cerr << "[shaderc] compile error in " << "test.glsl" << ":\n" << result.GetErrorMessage() << std::endl; - } - - // load the vertex shader code - std::vector vertexCode{result.cbegin(), result.cend()}; - - // create the vertex shader - SDL_GPUShaderCreateInfo vertexInfo{}; - vertexInfo.code = (Uint8*)vertexCode.data(); - vertexInfo.code_size = vertexCode.size() * 4; - vertexInfo.entrypoint = "main"; - vertexInfo.format = SDL_GPU_SHADERFORMAT_SPIRV; - vertexInfo.stage = SDL_GPU_SHADERSTAGE_VERTEX; - vertexInfo.num_samplers = 0; - vertexInfo.num_storage_buffers = 0; - vertexInfo.num_storage_textures = 0; - vertexInfo.num_uniform_buffers = 0; - - vertexShader = SDL_CreateGPUShader(device, &vertexInfo); - - result = compiler.CompileGlslToSpv(fragment_source, shaderc_glsl_fragment_shader, "test.frag", options); - - if (result.GetCompilationStatus() != shaderc_compilation_status_success) - { - std::cerr << "[shaderc] compile error in " << "test.frag" << ":\n" << result.GetErrorMessage() << std::endl; - } - - // load the fragment shader code - std::vector fragmentCode{result.cbegin(), result.cend()}; - - // create the fragment shader - SDL_GPUShaderCreateInfo fragmentInfo{}; - fragmentInfo.code = (Uint8*)fragmentCode.data(); - fragmentInfo.code_size = fragmentCode.size() * 4; - fragmentInfo.entrypoint = "main"; - fragmentInfo.format = SDL_GPU_SHADERFORMAT_SPIRV; - fragmentInfo.stage = SDL_GPU_SHADERSTAGE_FRAGMENT; - fragmentInfo.num_samplers = 0; - fragmentInfo.num_storage_buffers = 0; - fragmentInfo.num_storage_textures = 0; - fragmentInfo.num_uniform_buffers = 0; - - fragmentShader = SDL_CreateGPUShader(device, &fragmentInfo); - - // create the graphics pipeline - pipelineInfo.vertex_shader = vertexShader; - pipelineInfo.fragment_shader = fragmentShader; - pipelineInfo.primitive_type = SDL_GPU_PRIMITIVETYPE_TRIANGLELIST; + SDL_ClaimWindowForGPUDevice(gpu_wrapper->data(), window); + pipeline_wrapper.emplace(gpu_wrapper); // describe the vertex buffers vertexBufferDesctiptions[0].slot = 0; + vertexBufferDesctiptions[0].pitch = sizeof(Vertex); vertexBufferDesctiptions[0].input_rate = SDL_GPU_VERTEXINPUTRATE_VERTEX; vertexBufferDesctiptions[0].instance_step_rate = 0; - vertexBufferDesctiptions[0].pitch = sizeof(Vertex); pipelineInfo.vertex_input_state.num_vertex_buffers = 1; pipelineInfo.vertex_input_state.vertex_buffer_descriptions = vertexBufferDesctiptions; // describe the vertex attribute // a_position - vertexAttributes[0].buffer_slot = 0; vertexAttributes[0].location = 0; + vertexAttributes[0].buffer_slot = 0; vertexAttributes[0].format = SDL_GPU_VERTEXELEMENTFORMAT_FLOAT3; vertexAttributes[0].offset = 0; @@ -159,28 +116,26 @@ void main() // describe the color target colorTargetDescriptions[0] = {}; - colorTargetDescriptions[0].blend_state.enable_blend = true; - colorTargetDescriptions[0].blend_state.color_blend_op = SDL_GPU_BLENDOP_ADD; - colorTargetDescriptions[0].blend_state.alpha_blend_op = SDL_GPU_BLENDOP_ADD; colorTargetDescriptions[0].blend_state.src_color_blendfactor = SDL_GPU_BLENDFACTOR_SRC_ALPHA; colorTargetDescriptions[0].blend_state.dst_color_blendfactor = SDL_GPU_BLENDFACTOR_ONE_MINUS_SRC_ALPHA; + colorTargetDescriptions[0].blend_state.color_blend_op = SDL_GPU_BLENDOP_ADD; colorTargetDescriptions[0].blend_state.src_alpha_blendfactor = SDL_GPU_BLENDFACTOR_SRC_ALPHA; colorTargetDescriptions[0].blend_state.dst_alpha_blendfactor = SDL_GPU_BLENDFACTOR_ONE_MINUS_SRC_ALPHA; - colorTargetDescriptions[0].format = SDL_GetGPUSwapchainTextureFormat(device, window); + colorTargetDescriptions[0].blend_state.alpha_blend_op = SDL_GPU_BLENDOP_ADD; + colorTargetDescriptions[0].blend_state.enable_blend = true; + colorTargetDescriptions[0].format = SDL_GetGPUSwapchainTextureFormat(gpu_wrapper->data(), window); pipelineInfo.target_info.num_color_targets = 1; pipelineInfo.target_info.color_target_descriptions = colorTargetDescriptions; // create the pipeline - graphicsPipeline = SDL_CreateGPUGraphicsPipeline(device, &pipelineInfo); + // graphicsPipeline = SDL_CreateGPUGraphicsPipeline(gpu_wrapper->data(), &pipelineInfo); + pipeline_wrapper->set_vertex_shader(vertex_source); + pipeline_wrapper->set_fragment_shader(fragment_source); + pipeline_wrapper->submit(); - // create the vertex buffer - SDL_GPUBufferCreateInfo bufferInfo{}; - bufferInfo.size = sizeof(vertices); - bufferInfo.usage = SDL_GPU_BUFFERUSAGE_VERTEX; - vertexBuffer.emplace(device, &bufferInfo); + vertex_buffer.upload(&vertices, sizeof(vertices), 0); - vertexBuffer->upload(&vertices, sizeof(vertices), 0); // Setup Dear ImGui context IMGUI_CHECKVERSION(); @@ -207,8 +162,8 @@ void main() // Setup Platform/Renderer backends ImGui_ImplSDL3_InitForSDLGPU(window); ImGui_ImplSDLGPU3_InitInfo init_info = {}; - init_info.Device = device; - init_info.ColorTargetFormat = SDL_GetGPUSwapchainTextureFormat(device, window); + init_info.Device = gpu_wrapper->data(); + init_info.ColorTargetFormat = SDL_GetGPUSwapchainTextureFormat(gpu_wrapper->data(), 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. init_info.PresentMode = SDL_GPU_PRESENTMODE_VSYNC; @@ -217,6 +172,16 @@ void main() return SDL_APP_CONTINUE; } + /** + * @brief Advance the application by one frame: update UI, apply vertex edits and live shader recompilation, render, + * and submit GPU work. + * + * Processes ImGui frames, uploads vertex data when edited, recompiles and replaces the vertex shader and graphics + * pipeline on shader edits, records a render pass that draws the triangle and ImGui draw lists, and submits the GPU + * command buffer for presentation. + * + * @return `SDL_APP_CONTINUE` to continue the main loop. + */ virtual SDL_AppResult iterate() override { ImGui_ImplSDLGPU3_NewFrame(); @@ -232,7 +197,7 @@ void main() change = ImGui::DragFloat3("node3", vertices[2].position(), 0.01f, -1.f, 1.f) || change; if (change) { - vertexBuffer->upload(&vertices, sizeof(vertices), 0); + vertex_buffer.upload(&vertices, sizeof(vertices), 0); } ImGui::End(); } @@ -244,39 +209,7 @@ void main() std::min(ImGui::GetTextLineHeight() * (line_count + 3), ImGui::GetContentRegionAvail().y)); if (ImGui::InputTextMultiline("code editor", &vertex_source, size, ImGuiInputTextFlags_AllowTabInput)) { - auto result = - compiler.CompileGlslToSpv(vertex_source, shaderc_glsl_vertex_shader, "test.glsl", options); - - if (result.GetCompilationStatus() != shaderc_compilation_status_success) - { - std::cerr << "[shaderc] compile error in " << "test.glsl" << ":\n" - << result.GetErrorMessage() << std::endl; - } - else - { - // load the vertex shader code - std::vector vertexCode{result.cbegin(), result.cend()}; - - // create the vertex shader - SDL_GPUShaderCreateInfo vertexInfo{}; - vertexInfo.code = (Uint8*)vertexCode.data(); - vertexInfo.code_size = vertexCode.size() * 4; - vertexInfo.entrypoint = "main"; - vertexInfo.format = SDL_GPU_SHADERFORMAT_SPIRV; - vertexInfo.stage = SDL_GPU_SHADERSTAGE_VERTEX; - vertexInfo.num_samplers = 0; - vertexInfo.num_storage_buffers = 0; - vertexInfo.num_storage_textures = 0; - vertexInfo.num_uniform_buffers = 0; - - - SDL_ReleaseGPUShader(device, vertexShader); - vertexShader = SDL_CreateGPUShader(device, &vertexInfo); - - pipelineInfo.vertex_shader = vertexShader; - SDL_ReleaseGPUGraphicsPipeline(device, graphicsPipeline); - graphicsPipeline = SDL_CreateGPUGraphicsPipeline(device, &pipelineInfo); - } + pipeline_wrapper->set_vertex_shader(vertex_source); } ImGui::End(); @@ -285,8 +218,10 @@ void main() ImGui::Render(); ImDrawData* draw_data = ImGui::GetDrawData(); + pipeline_wrapper->submit(); + // acquire the command buffer - SDL_GPUCommandBuffer* commandBuffer = SDL_AcquireGPUCommandBuffer(device); + SDL_GPUCommandBuffer* commandBuffer = SDL_AcquireGPUCommandBuffer(gpu_wrapper->data()); // get the swapchain texture SDL_GPUTexture* swapchainTexture; @@ -314,11 +249,11 @@ void main() SDL_GPURenderPass* renderPass = SDL_BeginGPURenderPass(commandBuffer, &colorTargetInfo, 1, NULL); // draw calls go here - SDL_BindGPUGraphicsPipeline(renderPass, graphicsPipeline); + SDL_BindGPUGraphicsPipeline(renderPass, pipeline_wrapper->data()); // bind the vertex buffer SDL_GPUBufferBinding bufferBindings[1]; - bufferBindings[0].buffer = vertexBuffer->data(); // index 0 is slot 0 in this example + bufferBindings[0].buffer = vertex_buffer.data(); // index 0 is slot 0 in this example bufferBindings[0].offset = 0; // start from the first byte SDL_BindGPUVertexBuffers(renderPass, 0, bufferBindings, 1); // bind one buffer starting from slot 0 @@ -335,6 +270,12 @@ void main() return SDL_APP_CONTINUE; } + /** + * @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 { ImGui_ImplSDL3_ProcessEvent(event); @@ -347,23 +288,22 @@ 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 { ImGui_ImplSDL3_Shutdown(); ImGui_ImplSDLGPU3_Shutdown(); ImGui::DestroyContext(); - // release buffers - vertexBuffer = std::nullopt; - - // Release the shader - SDL_ReleaseGPUShader(device, vertexShader); - SDL_ReleaseGPUShader(device, fragmentShader); - - // release the pipeline - SDL_ReleaseGPUGraphicsPipeline(device, graphicsPipeline); - // destroy the GPU device - SDL_DestroyGPUDevice(device); + SDL_ReleaseWindowFromGPUDevice(gpu_wrapper->data(), window); // destroy the window SDL_DestroyWindow(window); diff --git a/sdl_wrapper/CMakeLists.txt b/sdl_wrapper/CMakeLists.txt index 796b21f..5beae00 100644 --- a/sdl_wrapper/CMakeLists.txt +++ b/sdl_wrapper/CMakeLists.txt @@ -10,8 +10,13 @@ target_sources(sdl_wrapper sdl_wrapper.ixx sdl_wrapper.buffer.ixx sdl_wrapper.app.ixx + sdl_wrapper.pipeline.ixx + sdl_wrapper.gpu.ixx + sdl_wrapper.decl.ixx PRIVATE + sdl_wrapper.buffer.cpp + sdl_wrapper.pipeline.cpp sdl_callback_implement.cpp ) -target_link_libraries(sdl_wrapper PUBLIC SDL3::SDL3) +target_link_libraries(sdl_wrapper PUBLIC SDL3::SDL3 shaderc) diff --git a/sdl_wrapper/sdl_callback_implement.cpp b/sdl_wrapper/sdl_callback_implement.cpp index 17e6843..c684dea 100644 --- a/sdl_wrapper/sdl_callback_implement.cpp +++ b/sdl_wrapper/sdl_callback_implement.cpp @@ -6,28 +6,63 @@ #include import sdl_wrapper; -extern sopho::App *create_app(); +extern sopho::App* create_app(); -SDL_AppResult SDL_AppInit(void **appstate, int argc, char **argv) { - auto app = create_app(); - if (!app) - return SDL_APP_FAILURE; - *appstate = app; - return app->init(argc, argv); +/** + * @brief Initializes the SDL video subsystem, constructs the application, and invokes its initialization. + * + * @param appstate Pointer to storage that will receive the created sopho::App* on success. + * @param argc Program argument count forwarded to the application's init. + * @param argv Program argument vector forwarded to the application's init. + * @return SDL_AppResult Result returned by the application's init, or SDL_APP_FAILURE if application creation failed. + */ +SDL_AppResult SDL_AppInit(void** appstate, int argc, char** argv) +{ + SDL_Init(SDL_INIT_VIDEO); + auto app = create_app(); + if (!app) + return SDL_APP_FAILURE; + *appstate = app; + return app->init(argc, argv); } -SDL_AppResult SDL_AppIterate(void *appstate) { - auto *app = static_cast(appstate); - return app->iterate(); +/** + * @brief Run a single per-frame iteration on the application. + * + * @param appstate Pointer to the sopho::App instance stored by SDL_AppInit. + * @return SDL_AppResult The application's requested next action. + */ +SDL_AppResult SDL_AppIterate(void* appstate) +{ + auto* app = static_cast(appstate); + return app->iterate(); } -SDL_AppResult SDL_AppEvent(void *appstate, SDL_Event *event) { - auto *app = static_cast(appstate); - return app->event(event); +/** + * @brief Dispatches an SDL event to the stored application instance. + * + * @param appstate Opaque pointer previously set by SDL_AppInit; must point to a `sopho::App` instance. + * @param event Pointer to the SDL event to deliver to the application. + * @return SDL_AppResult Result returned by the application's event handler. + */ +SDL_AppResult SDL_AppEvent(void* appstate, SDL_Event* event) +{ + auto* app = static_cast(appstate); + return app->event(event); } -void SDL_AppQuit(void *appstate, SDL_AppResult result) { - auto *app = static_cast(appstate); - app->quit(result); - delete app; +/** + * @brief Shuts down the application and releases its instance. + * + * Invokes the application's quit handler with the provided result and destroys + * the stored application instance. + * + * @param appstate Pointer to the sopho::App instance previously stored by SDL_AppInit. + * @param result Result code describing why the application is quitting. + */ +void SDL_AppQuit(void* appstate, SDL_AppResult result) +{ + auto* app = static_cast(appstate); + app->quit(result); + delete app; } diff --git a/sdl_wrapper/sdl_wrapper.app.ixx b/sdl_wrapper/sdl_wrapper.app.ixx index 5ac5bc3..253d070 100644 --- a/sdl_wrapper/sdl_wrapper.app.ixx +++ b/sdl_wrapper/sdl_wrapper.app.ixx @@ -4,17 +4,19 @@ module; #include "SDL3/SDL_init.h" export module sdl_wrapper:app; -namespace sopho { - export class App { +export namespace sopho +{ + class App + { public: virtual ~App() = default; - virtual SDL_AppResult init(int argc, char **argv) = 0; + virtual SDL_AppResult init(int argc, char** argv) = 0; virtual SDL_AppResult iterate() = 0; - virtual SDL_AppResult event(SDL_Event *event) = 0; + virtual SDL_AppResult event(SDL_Event* event) = 0; virtual void quit(SDL_AppResult result) = 0; }; -} +} // namespace sopho diff --git a/sdl_wrapper/sdl_wrapper.buffer.cpp b/sdl_wrapper/sdl_wrapper.buffer.cpp new file mode 100644 index 0000000..38e3cb1 --- /dev/null +++ b/sdl_wrapper/sdl_wrapper.buffer.cpp @@ -0,0 +1,82 @@ +// +// Created by sophomore on 11/12/25. +// +module; +#include +#include "SDL3/SDL_gpu.h" +#include "SDL3/SDL_stdinc.h" +module sdl_wrapper; +import :buffer; +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() + { + SDL_ReleaseGPUBuffer(m_gpu->data(), m_vertex_buffer); + m_vertex_buffer = nullptr; + if (m_transfer_buffer) + { + SDL_ReleaseGPUTransferBuffer(m_gpu->data(), 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) + { + if (p_size > m_transfer_buffer_size) + { + if (m_transfer_buffer != nullptr) + { + SDL_ReleaseGPUTransferBuffer(m_gpu->data(), m_transfer_buffer); + } + 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); + + // TODO: Delay submit command in collect + auto command_buffer = SDL_AcquireGPUCommandBuffer(m_gpu->data()); + auto copy_pass = SDL_BeginGPUCopyPass(command_buffer); + + SDL_GPUTransferBufferLocation location{}; + location.transfer_buffer = m_transfer_buffer; + location.offset = 0; + + SDL_GPUBufferRegion region{}; + region.buffer = m_vertex_buffer; + region.size = p_size; + region.offset = p_offset; + + SDL_UploadToGPUBuffer(copy_pass, &location, ®ion, false); + + SDL_EndGPUCopyPass(copy_pass); + SDL_SubmitGPUCommandBuffer(command_buffer); + } +} // namespace sopho diff --git a/sdl_wrapper/sdl_wrapper.buffer.ixx b/sdl_wrapper/sdl_wrapper.buffer.ixx index 845436f..a813db7 100644 --- a/sdl_wrapper/sdl_wrapper.buffer.ixx +++ b/sdl_wrapper/sdl_wrapper.buffer.ixx @@ -2,69 +2,31 @@ // Created by sophomore on 11/8/25. // module; +#include #include "SDL3/SDL_gpu.h" export module sdl_wrapper:buffer; - -namespace sopho { - export class BufferWrapper { - SDL_GPUDevice *m_gpu{}; - SDL_GPUBuffer *m_vertex_buffer{}; - SDL_GPUTransferBuffer *m_transfer_buffer{}; +import :decl; + +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{}; - public: - BufferWrapper() = default; - - BufferWrapper(SDL_GPUDevice *p_gpu, const SDL_GPUBufferCreateInfo *p_create_info) - : m_gpu(p_gpu), m_vertex_buffer(SDL_CreateGPUBuffer(p_gpu, p_create_info)) { + BufferWrapper(std::shared_ptr p_gpu, SDL_GPUBuffer* p_buffer) : + m_gpu(p_gpu), m_vertex_buffer(p_buffer) + { } - void upload(void *p_data, uint32_t p_size, uint32_t p_offset) { - if (p_size > m_transfer_buffer_size) { - if (m_transfer_buffer != nullptr) { - SDL_ReleaseGPUTransferBuffer(m_gpu, m_transfer_buffer); - } - SDL_GPUTransferBufferCreateInfo transfer_info{SDL_GPU_TRANSFERBUFFERUSAGE_UPLOAD, p_size, 0}; - m_transfer_buffer = SDL_CreateGPUTransferBuffer(m_gpu, &transfer_info); - m_transfer_buffer_size = transfer_info.size; - } - - auto data = SDL_MapGPUTransferBuffer(m_gpu, m_transfer_buffer, false); - SDL_memcpy(data, p_data, p_size); - SDL_UnmapGPUTransferBuffer(m_gpu, m_transfer_buffer); - - // TODO: Delay submit command in collect - auto command_buffer = SDL_AcquireGPUCommandBuffer(m_gpu); - auto copy_pass = SDL_BeginGPUCopyPass(command_buffer); - - SDL_GPUTransferBufferLocation location{}; - location.transfer_buffer = m_transfer_buffer; - location.offset = 0; - - SDL_GPUBufferRegion region{}; - region.buffer = m_vertex_buffer; - region.size = p_size; - region.offset = p_offset; - - SDL_UploadToGPUBuffer(copy_pass, &location, ®ion, false); - - SDL_EndGPUCopyPass(copy_pass); - SDL_SubmitGPUCommandBuffer(command_buffer); - } + public: + void upload(void* p_data, uint32_t p_size, uint32_t p_offset); - auto data() { - return m_vertex_buffer; - } + auto data() { return m_vertex_buffer; } - ~BufferWrapper() { - // TODO: It's too late to release gpu buffer, gpu was released - SDL_ReleaseGPUBuffer(m_gpu, m_vertex_buffer); - m_vertex_buffer = nullptr; - if (m_transfer_buffer) { - SDL_ReleaseGPUTransferBuffer(m_gpu, m_transfer_buffer); - m_transfer_buffer = nullptr; - m_transfer_buffer_size = 0; - } - } + ~BufferWrapper(); + friend GpuWrapper; }; -} +} // namespace sopho diff --git a/sdl_wrapper/sdl_wrapper.decl.ixx b/sdl_wrapper/sdl_wrapper.decl.ixx new file mode 100644 index 0000000..0ac8bce --- /dev/null +++ b/sdl_wrapper/sdl_wrapper.decl.ixx @@ -0,0 +1,13 @@ +// +// Created by sophomore on 11/12/25. +// + +export module sdl_wrapper:decl; + +export namespace sopho +{ + class App; + class GpuWrapper; + class BufferWrapper; + class PipelineWrapper; +} diff --git a/sdl_wrapper/sdl_wrapper.gpu.ixx b/sdl_wrapper/sdl_wrapper.gpu.ixx new file mode 100644 index 0000000..c6c1694 --- /dev/null +++ b/sdl_wrapper/sdl_wrapper.gpu.ixx @@ -0,0 +1,83 @@ +// +// Created by sophomore on 11/12/25. +// +module; +#include +#include "SDL3/SDL_gpu.h" +#include "SDL3/SDL_log.h" +#include "shaderc/shaderc.hpp" +export module sdl_wrapper:gpu; +import :buffer; +import :pipeline; +namespace sopho +{ + export class GpuWrapper : public std::enable_shared_from_this + { + SDL_GPUDevice* m_device{}; + + SDL_Window* m_window{}; + + public: + GpuWrapper() : m_device(SDL_CreateGPUDevice(SDL_GPU_SHADERFORMAT_SPIRV, true, nullptr)) + { + if (m_device == nullptr) + { + SDL_LogError(SDL_LOG_CATEGORY_GPU, "%s:%d %s", __FILE__, __LINE__, SDL_GetError()); + } + } + + ~GpuWrapper() + { + if (m_device) + { + SDL_DestroyGPUDevice(m_device); + } + m_device = nullptr; + } + + auto data() { return m_device; } + + auto 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; + } + + auto create_pipeline() { return PipelineWrapper{shared_from_this()}; } + + auto create_shader(const std::vector& p_shader, SDL_GPUShaderStage p_stage) + { + 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 = 0; + return SDL_CreateGPUShader(m_device, &vertexInfo); + } + + auto release_shader(SDL_GPUShader* shader) + { + if (shader) + { + SDL_ReleaseGPUShader(m_device, shader); + } + } + + auto get_texture_format() + { + return SDL_GetGPUSwapchainTextureFormat(m_device, m_window); + } + + auto set_window(SDL_Window* p_window) + { + m_window = p_window; + } + }; +} // namespace sopho diff --git a/sdl_wrapper/sdl_wrapper.ixx b/sdl_wrapper/sdl_wrapper.ixx index 27f47c6..9e7a9fd 100644 --- a/sdl_wrapper/sdl_wrapper.ixx +++ b/sdl_wrapper/sdl_wrapper.ixx @@ -3,5 +3,8 @@ // export module sdl_wrapper; -export import :buffer; +export import :decl; export import :app; +export import :gpu; +export import :buffer; +export import :pipeline; diff --git a/sdl_wrapper/sdl_wrapper.pipeline.cpp b/sdl_wrapper/sdl_wrapper.pipeline.cpp new file mode 100644 index 0000000..3978a43 --- /dev/null +++ b/sdl_wrapper/sdl_wrapper.pipeline.cpp @@ -0,0 +1,162 @@ +// +// Created by sophomore on 11/13/25. +// +module; +#include +#include "SDL3/SDL_gpu.h" +#include "SDL3/SDL_log.h" +#include "shaderc/shaderc.hpp" +module sdl_wrapper; +import :pipeline; + +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 p_device) : m_device(p_device) + { + options.SetTargetEnvironment(shaderc_target_env_vulkan, 0); + + 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_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(); + + m_pipeline_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}); + + 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(); + + 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() + { + if (m_graphics_pipeline) + { + SDL_ReleaseGPUGraphicsPipeline(m_device->data(), m_graphics_pipeline); + m_graphics_pipeline = nullptr; + } + m_device->release_shader(m_vertex_shader); + m_device->release_shader(m_fragment_shader); + } + /** + * @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() + { + if (m_modified) + { + 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()); + } + } + } + + /** + * @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) + { + auto result = compiler.CompileGlslToSpv(p_source, shaderc_glsl_vertex_shader, "vertex.glsl", options); + if (result.GetCompilationStatus() != shaderc_compilation_status_success) + { + SDL_LogError(SDL_LOG_CATEGORY_RENDER, "[shaderc] compile error in vertex.glsl: %s", + result.GetErrorMessage().data()); + } + else + { + 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); + m_pipeline_info.vertex_shader = m_vertex_shader; + m_modified = true; + } + } + /** + * @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) + { + auto result = compiler.CompileGlslToSpv(p_source, shaderc_glsl_fragment_shader, "fragment.glsl", options); + if (result.GetCompilationStatus() != shaderc_compilation_status_success) + { + SDL_LogError(SDL_LOG_CATEGORY_RENDER, "[shaderc] compile error fragment.glsl: %s", + result.GetErrorMessage().data()); + } + else + { + 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); + m_pipeline_info.fragment_shader = m_fragment_shader; + m_modified = true; + } + } +} // namespace sopho diff --git a/sdl_wrapper/sdl_wrapper.pipeline.ixx b/sdl_wrapper/sdl_wrapper.pipeline.ixx new file mode 100644 index 0000000..8b58e31 --- /dev/null +++ b/sdl_wrapper/sdl_wrapper.pipeline.ixx @@ -0,0 +1,43 @@ +// +// Created by sophomore on 11/11/25. +// +module; +#include +#include "SDL3/SDL_gpu.h" +#include "shaderc/shaderc.hpp" +export module sdl_wrapper:pipeline; +import :decl; +export namespace sopho +{ + class PipelineWrapper + { + SDL_GPUGraphicsPipeline* m_graphics_pipeline{}; + std::shared_ptr m_device{}; + + 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{}; + SDL_GPUGraphicsPipelineCreateInfo m_pipeline_info{}; + + shaderc::Compiler compiler{}; + shaderc::CompileOptions options{}; + + bool m_modified = false; + + public: + PipelineWrapper(std::shared_ptr p_device); + PipelineWrapper(const PipelineWrapper&)=default; + PipelineWrapper(PipelineWrapper&&)=default; + ~PipelineWrapper(); + + auto data() { return m_graphics_pipeline; } + + void submit(); + + void set_vertex_shader(const std::string& p_source); + void set_fragment_shader(const std::string& p_source); + friend GpuWrapper; + }; +} // namespace sopho