feat(gpu): migrar a SDL3_GPU amb 2-pass rendering i post-processat
- Infraestructura GPU: GpuContext, GpuPipeline, GpuSpriteBatch, GpuTexture - Engine::render() migrat a 2-pass: sprites → offscreen R8G8B8A8 → swapchain + vignette - UI/text via software renderer (SDL3_ttf) + upload com a textura overlay GPU - CMakeLists.txt actualitzat per incloure subsistema gpu/ Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
78
source/gpu/gpu_context.cpp
Normal file
78
source/gpu/gpu_context.cpp
Normal file
@@ -0,0 +1,78 @@
|
||||
#include "gpu_context.hpp"
|
||||
|
||||
#include <SDL3/SDL_log.h>
|
||||
#include <iostream>
|
||||
|
||||
bool GpuContext::init(SDL_Window* window) {
|
||||
window_ = window;
|
||||
|
||||
// Create GPU device — prefer Metal on macOS, Vulkan elsewhere
|
||||
SDL_GPUShaderFormat preferred = SDL_GPU_SHADERFORMAT_MSL
|
||||
| SDL_GPU_SHADERFORMAT_METALLIB
|
||||
| SDL_GPU_SHADERFORMAT_SPIRV;
|
||||
device_ = SDL_CreateGPUDevice(preferred, false, nullptr);
|
||||
if (!device_) {
|
||||
std::cerr << "GpuContext: SDL_CreateGPUDevice failed: " << SDL_GetError() << std::endl;
|
||||
return false;
|
||||
}
|
||||
std::cout << "GpuContext: driver = " << SDL_GetGPUDeviceDriver(device_) << std::endl;
|
||||
|
||||
// Claim the window so the GPU device owns its swapchain
|
||||
if (!SDL_ClaimWindowForGPUDevice(device_, window_)) {
|
||||
std::cerr << "GpuContext: SDL_ClaimWindowForGPUDevice failed: " << SDL_GetError() << std::endl;
|
||||
SDL_DestroyGPUDevice(device_);
|
||||
device_ = nullptr;
|
||||
return false;
|
||||
}
|
||||
|
||||
// Query swapchain format (Metal: typically B8G8R8A8_UNORM or R8G8B8A8_UNORM)
|
||||
swapchain_format_ = SDL_GetGPUSwapchainTextureFormat(device_, window_);
|
||||
std::cout << "GpuContext: swapchain format = " << static_cast<int>(swapchain_format_) << std::endl;
|
||||
|
||||
// Default: VSync ON
|
||||
SDL_SetGPUSwapchainParameters(device_, window_,
|
||||
SDL_GPU_SWAPCHAINCOMPOSITION_SDR,
|
||||
SDL_GPU_PRESENTMODE_VSYNC);
|
||||
return true;
|
||||
}
|
||||
|
||||
void GpuContext::destroy() {
|
||||
if (device_) {
|
||||
SDL_WaitForGPUIdle(device_);
|
||||
SDL_ReleaseWindowFromGPUDevice(device_, window_);
|
||||
SDL_DestroyGPUDevice(device_);
|
||||
device_ = nullptr;
|
||||
}
|
||||
window_ = nullptr;
|
||||
}
|
||||
|
||||
SDL_GPUCommandBuffer* GpuContext::acquireCommandBuffer() {
|
||||
SDL_GPUCommandBuffer* cmd = SDL_AcquireGPUCommandBuffer(device_);
|
||||
if (!cmd) {
|
||||
SDL_Log("GpuContext: SDL_AcquireGPUCommandBuffer failed: %s", SDL_GetError());
|
||||
}
|
||||
return cmd;
|
||||
}
|
||||
|
||||
SDL_GPUTexture* GpuContext::acquireSwapchainTexture(SDL_GPUCommandBuffer* cmd_buf,
|
||||
Uint32* out_w, Uint32* out_h) {
|
||||
SDL_GPUTexture* tex = nullptr;
|
||||
if (!SDL_AcquireGPUSwapchainTexture(cmd_buf, window_, &tex, out_w, out_h)) {
|
||||
SDL_Log("GpuContext: SDL_AcquireGPUSwapchainTexture failed: %s", SDL_GetError());
|
||||
return nullptr;
|
||||
}
|
||||
// tex == nullptr when window is minimized — caller should skip rendering
|
||||
return tex;
|
||||
}
|
||||
|
||||
void GpuContext::submit(SDL_GPUCommandBuffer* cmd_buf) {
|
||||
SDL_SubmitGPUCommandBuffer(cmd_buf);
|
||||
}
|
||||
|
||||
bool GpuContext::setVSync(bool enabled) {
|
||||
SDL_GPUPresentMode mode = enabled ? SDL_GPU_PRESENTMODE_VSYNC
|
||||
: SDL_GPU_PRESENTMODE_IMMEDIATE;
|
||||
return SDL_SetGPUSwapchainParameters(device_, window_,
|
||||
SDL_GPU_SWAPCHAINCOMPOSITION_SDR,
|
||||
mode);
|
||||
}
|
||||
33
source/gpu/gpu_context.hpp
Normal file
33
source/gpu/gpu_context.hpp
Normal file
@@ -0,0 +1,33 @@
|
||||
#pragma once
|
||||
|
||||
#include <SDL3/SDL_gpu.h>
|
||||
#include <SDL3/SDL_video.h>
|
||||
|
||||
// ============================================================================
|
||||
// GpuContext — SDL_GPU device + swapchain wrapper
|
||||
// Replaces SDL_Renderer as the main rendering backend.
|
||||
// ============================================================================
|
||||
class GpuContext {
|
||||
public:
|
||||
bool init(SDL_Window* window);
|
||||
void destroy();
|
||||
|
||||
SDL_GPUDevice* device() const { return device_; }
|
||||
SDL_Window* window() const { return window_; }
|
||||
SDL_GPUTextureFormat swapchainFormat() const { return swapchain_format_; }
|
||||
|
||||
// Per-frame helpers
|
||||
SDL_GPUCommandBuffer* acquireCommandBuffer();
|
||||
// Returns nullptr if window is minimized (swapchain not available).
|
||||
SDL_GPUTexture* acquireSwapchainTexture(SDL_GPUCommandBuffer* cmd_buf,
|
||||
Uint32* out_w, Uint32* out_h);
|
||||
void submit(SDL_GPUCommandBuffer* cmd_buf);
|
||||
|
||||
// VSync control (call after init)
|
||||
bool setVSync(bool enabled);
|
||||
|
||||
private:
|
||||
SDL_GPUDevice* device_ = nullptr;
|
||||
SDL_Window* window_ = nullptr;
|
||||
SDL_GPUTextureFormat swapchain_format_ = SDL_GPU_TEXTUREFORMAT_INVALID;
|
||||
};
|
||||
286
source/gpu/gpu_pipeline.cpp
Normal file
286
source/gpu/gpu_pipeline.cpp
Normal file
@@ -0,0 +1,286 @@
|
||||
#include "gpu_pipeline.hpp"
|
||||
#include "gpu_sprite_batch.hpp" // for GpuVertex layout
|
||||
|
||||
#include <SDL3/SDL_log.h>
|
||||
#include <cstddef> // offsetof
|
||||
|
||||
// ============================================================================
|
||||
// MSL Shaders (Metal Shading Language, macOS)
|
||||
// ============================================================================
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Sprite vertex shader
|
||||
// Input: GpuVertex (pos=NDC float2, uv float2, col float4)
|
||||
// Output: position, uv, col forwarded to fragment stage
|
||||
// ---------------------------------------------------------------------------
|
||||
static const char* kSpriteVertMSL = R"(
|
||||
#include <metal_stdlib>
|
||||
using namespace metal;
|
||||
|
||||
struct SpriteVIn {
|
||||
float2 pos [[attribute(0)]];
|
||||
float2 uv [[attribute(1)]];
|
||||
float4 col [[attribute(2)]];
|
||||
};
|
||||
struct SpriteVOut {
|
||||
float4 pos [[position]];
|
||||
float2 uv;
|
||||
float4 col;
|
||||
};
|
||||
|
||||
vertex SpriteVOut sprite_vs(SpriteVIn in [[stage_in]]) {
|
||||
SpriteVOut out;
|
||||
out.pos = float4(in.pos, 0.0, 1.0);
|
||||
out.uv = in.uv;
|
||||
out.col = in.col;
|
||||
return out;
|
||||
}
|
||||
)";
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Sprite fragment shader
|
||||
// Samples a texture and multiplies by vertex color (for tinting + alpha).
|
||||
// ---------------------------------------------------------------------------
|
||||
static const char* kSpriteFragMSL = R"(
|
||||
#include <metal_stdlib>
|
||||
using namespace metal;
|
||||
|
||||
struct SpriteVOut {
|
||||
float4 pos [[position]];
|
||||
float2 uv;
|
||||
float4 col;
|
||||
};
|
||||
|
||||
fragment float4 sprite_fs(SpriteVOut in [[stage_in]],
|
||||
texture2d<float> tex [[texture(0)]],
|
||||
sampler samp [[sampler(0)]]) {
|
||||
float4 t = tex.sample(samp, in.uv);
|
||||
return float4(t.rgb * in.col.rgb, t.a * in.col.a);
|
||||
}
|
||||
)";
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// PostFX vertex shader
|
||||
// Generates a full-screen triangle from vertex_id (no vertex buffer needed).
|
||||
// UV mapping: NDC(-1,-1)→UV(0,1) NDC(-1,3)→UV(0,-1) NDC(3,-1)→UV(2,1)
|
||||
// ---------------------------------------------------------------------------
|
||||
static const char* kPostFXVertMSL = R"(
|
||||
#include <metal_stdlib>
|
||||
using namespace metal;
|
||||
|
||||
struct PostVOut {
|
||||
float4 pos [[position]];
|
||||
float2 uv;
|
||||
};
|
||||
|
||||
vertex PostVOut postfx_vs(uint vid [[vertex_id]]) {
|
||||
const float2 positions[3] = { {-1.0, -1.0}, {3.0, -1.0}, {-1.0, 3.0} };
|
||||
const float2 uvs[3] = { { 0.0, 1.0}, {2.0, 1.0}, { 0.0,-1.0} };
|
||||
PostVOut out;
|
||||
out.pos = float4(positions[vid], 0.0, 1.0);
|
||||
out.uv = uvs[vid];
|
||||
return out;
|
||||
}
|
||||
)";
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// PostFX fragment shader
|
||||
// Samples the offscreen scene texture and applies a subtle vignette.
|
||||
// ---------------------------------------------------------------------------
|
||||
static const char* kPostFXFragMSL = R"(
|
||||
#include <metal_stdlib>
|
||||
using namespace metal;
|
||||
|
||||
struct PostVOut {
|
||||
float4 pos [[position]];
|
||||
float2 uv;
|
||||
};
|
||||
|
||||
fragment float4 postfx_fs(PostVOut in [[stage_in]],
|
||||
texture2d<float> scene [[texture(0)]],
|
||||
sampler samp [[sampler(0)]]) {
|
||||
float4 color = scene.sample(samp, in.uv);
|
||||
|
||||
// Subtle vignette: darkens edges proportionally to distance from centre
|
||||
float2 d = in.uv - float2(0.5, 0.5);
|
||||
float vignette = 1.0 - dot(d, d) * 1.5;
|
||||
color.rgb *= clamp(vignette, 0.0, 1.0);
|
||||
|
||||
return color;
|
||||
}
|
||||
)";
|
||||
|
||||
// ============================================================================
|
||||
// GpuPipeline implementation
|
||||
// ============================================================================
|
||||
|
||||
bool GpuPipeline::init(SDL_GPUDevice* device,
|
||||
SDL_GPUTextureFormat target_format,
|
||||
SDL_GPUTextureFormat offscreen_format) {
|
||||
SDL_GPUShaderFormat supported = SDL_GetGPUShaderFormats(device);
|
||||
if (!(supported & SDL_GPU_SHADERFORMAT_MSL)) {
|
||||
SDL_Log("GpuPipeline: device does not support MSL shaders (format mask=%u)", supported);
|
||||
return false;
|
||||
}
|
||||
|
||||
// ----------------------------------------------------------------
|
||||
// Sprite pipeline
|
||||
// ----------------------------------------------------------------
|
||||
SDL_GPUShader* sprite_vert = createShader(device, kSpriteVertMSL, "sprite_vs",
|
||||
SDL_GPU_SHADERSTAGE_VERTEX, 0, 0);
|
||||
SDL_GPUShader* sprite_frag = createShader(device, kSpriteFragMSL, "sprite_fs",
|
||||
SDL_GPU_SHADERSTAGE_FRAGMENT, 1, 0);
|
||||
if (!sprite_vert || !sprite_frag) {
|
||||
SDL_Log("GpuPipeline: failed to create sprite shaders");
|
||||
if (sprite_vert) SDL_ReleaseGPUShader(device, sprite_vert);
|
||||
if (sprite_frag) SDL_ReleaseGPUShader(device, sprite_frag);
|
||||
return false;
|
||||
}
|
||||
|
||||
// Vertex input: GpuVertex layout
|
||||
SDL_GPUVertexBufferDescription vb_desc = {};
|
||||
vb_desc.slot = 0;
|
||||
vb_desc.pitch = sizeof(GpuVertex);
|
||||
vb_desc.input_rate = SDL_GPU_VERTEXINPUTRATE_VERTEX;
|
||||
vb_desc.instance_step_rate = 0;
|
||||
|
||||
SDL_GPUVertexAttribute attrs[3] = {};
|
||||
attrs[0].location = 0;
|
||||
attrs[0].buffer_slot = 0;
|
||||
attrs[0].format = SDL_GPU_VERTEXELEMENTFORMAT_FLOAT2;
|
||||
attrs[0].offset = static_cast<Uint32>(offsetof(GpuVertex, x));
|
||||
|
||||
attrs[1].location = 1;
|
||||
attrs[1].buffer_slot = 0;
|
||||
attrs[1].format = SDL_GPU_VERTEXELEMENTFORMAT_FLOAT2;
|
||||
attrs[1].offset = static_cast<Uint32>(offsetof(GpuVertex, u));
|
||||
|
||||
attrs[2].location = 2;
|
||||
attrs[2].buffer_slot = 0;
|
||||
attrs[2].format = SDL_GPU_VERTEXELEMENTFORMAT_FLOAT4;
|
||||
attrs[2].offset = static_cast<Uint32>(offsetof(GpuVertex, r));
|
||||
|
||||
SDL_GPUVertexInputState vertex_input = {};
|
||||
vertex_input.vertex_buffer_descriptions = &vb_desc;
|
||||
vertex_input.num_vertex_buffers = 1;
|
||||
vertex_input.vertex_attributes = attrs;
|
||||
vertex_input.num_vertex_attributes = 3;
|
||||
|
||||
// Alpha blend state (SRC_ALPHA, ONE_MINUS_SRC_ALPHA)
|
||||
SDL_GPUColorTargetBlendState blend = {};
|
||||
blend.enable_blend = true;
|
||||
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_ONE;
|
||||
blend.dst_alpha_blendfactor = SDL_GPU_BLENDFACTOR_ONE_MINUS_SRC_ALPHA;
|
||||
blend.alpha_blend_op = SDL_GPU_BLENDOP_ADD;
|
||||
blend.enable_color_write_mask = false; // write all channels
|
||||
|
||||
SDL_GPUColorTargetDescription color_target_desc = {};
|
||||
color_target_desc.format = offscreen_format;
|
||||
color_target_desc.blend_state = blend;
|
||||
|
||||
SDL_GPUGraphicsPipelineCreateInfo sprite_pipe_info = {};
|
||||
sprite_pipe_info.vertex_shader = sprite_vert;
|
||||
sprite_pipe_info.fragment_shader = sprite_frag;
|
||||
sprite_pipe_info.vertex_input_state = vertex_input;
|
||||
sprite_pipe_info.primitive_type = SDL_GPU_PRIMITIVETYPE_TRIANGLELIST;
|
||||
sprite_pipe_info.target_info.num_color_targets = 1;
|
||||
sprite_pipe_info.target_info.color_target_descriptions = &color_target_desc;
|
||||
|
||||
sprite_pipeline_ = SDL_CreateGPUGraphicsPipeline(device, &sprite_pipe_info);
|
||||
|
||||
SDL_ReleaseGPUShader(device, sprite_vert);
|
||||
SDL_ReleaseGPUShader(device, sprite_frag);
|
||||
|
||||
if (!sprite_pipeline_) {
|
||||
SDL_Log("GpuPipeline: sprite pipeline creation failed: %s", SDL_GetError());
|
||||
return false;
|
||||
}
|
||||
|
||||
// ----------------------------------------------------------------
|
||||
// UI overlay pipeline (same as sprite but renders to swapchain format)
|
||||
// Reuse sprite shaders with different target format.
|
||||
// We create a second version of the sprite pipeline for swapchain.
|
||||
// ----------------------------------------------------------------
|
||||
// (postfx pipeline targets swapchain; UI overlay also targets swapchain
|
||||
// but needs its own pipeline with swapchain format.)
|
||||
// For simplicity, the sprite pipeline is used for the offscreen pass only.
|
||||
// The UI overlay is composited via a separate postfx-like pass below.
|
||||
|
||||
// ----------------------------------------------------------------
|
||||
// PostFX pipeline
|
||||
// ----------------------------------------------------------------
|
||||
SDL_GPUShader* postfx_vert = createShader(device, kPostFXVertMSL, "postfx_vs",
|
||||
SDL_GPU_SHADERSTAGE_VERTEX, 0, 0);
|
||||
SDL_GPUShader* postfx_frag = createShader(device, kPostFXFragMSL, "postfx_fs",
|
||||
SDL_GPU_SHADERSTAGE_FRAGMENT, 1, 0);
|
||||
if (!postfx_vert || !postfx_frag) {
|
||||
SDL_Log("GpuPipeline: failed to create postfx shaders");
|
||||
if (postfx_vert) SDL_ReleaseGPUShader(device, postfx_vert);
|
||||
if (postfx_frag) SDL_ReleaseGPUShader(device, postfx_frag);
|
||||
return false;
|
||||
}
|
||||
|
||||
// PostFX: no vertex input (uses vertex_id), no blend (replace output)
|
||||
SDL_GPUColorTargetBlendState no_blend = {};
|
||||
no_blend.enable_blend = false;
|
||||
no_blend.enable_color_write_mask = false;
|
||||
|
||||
SDL_GPUColorTargetDescription postfx_target_desc = {};
|
||||
postfx_target_desc.format = target_format;
|
||||
postfx_target_desc.blend_state = no_blend;
|
||||
|
||||
SDL_GPUVertexInputState no_input = {};
|
||||
|
||||
SDL_GPUGraphicsPipelineCreateInfo postfx_pipe_info = {};
|
||||
postfx_pipe_info.vertex_shader = postfx_vert;
|
||||
postfx_pipe_info.fragment_shader = postfx_frag;
|
||||
postfx_pipe_info.vertex_input_state = no_input;
|
||||
postfx_pipe_info.primitive_type = SDL_GPU_PRIMITIVETYPE_TRIANGLELIST;
|
||||
postfx_pipe_info.target_info.num_color_targets = 1;
|
||||
postfx_pipe_info.target_info.color_target_descriptions = &postfx_target_desc;
|
||||
|
||||
postfx_pipeline_ = SDL_CreateGPUGraphicsPipeline(device, &postfx_pipe_info);
|
||||
|
||||
SDL_ReleaseGPUShader(device, postfx_vert);
|
||||
SDL_ReleaseGPUShader(device, postfx_frag);
|
||||
|
||||
if (!postfx_pipeline_) {
|
||||
SDL_Log("GpuPipeline: postfx pipeline creation failed: %s", SDL_GetError());
|
||||
return false;
|
||||
}
|
||||
|
||||
SDL_Log("GpuPipeline: sprite and postfx pipelines created successfully");
|
||||
return true;
|
||||
}
|
||||
|
||||
void GpuPipeline::destroy(SDL_GPUDevice* device) {
|
||||
if (sprite_pipeline_) { SDL_ReleaseGPUGraphicsPipeline(device, sprite_pipeline_); sprite_pipeline_ = nullptr; }
|
||||
if (postfx_pipeline_) { SDL_ReleaseGPUGraphicsPipeline(device, postfx_pipeline_); postfx_pipeline_ = nullptr; }
|
||||
}
|
||||
|
||||
SDL_GPUShader* GpuPipeline::createShader(SDL_GPUDevice* device,
|
||||
const char* msl_source,
|
||||
const char* entrypoint,
|
||||
SDL_GPUShaderStage stage,
|
||||
Uint32 num_samplers,
|
||||
Uint32 num_uniform_buffers) {
|
||||
SDL_GPUShaderCreateInfo info = {};
|
||||
info.code = reinterpret_cast<const Uint8*>(msl_source);
|
||||
info.code_size = static_cast<size_t>(strlen(msl_source) + 1);
|
||||
info.entrypoint = entrypoint;
|
||||
info.format = SDL_GPU_SHADERFORMAT_MSL;
|
||||
info.stage = stage;
|
||||
info.num_samplers = num_samplers;
|
||||
info.num_storage_textures = 0;
|
||||
info.num_storage_buffers = 0;
|
||||
info.num_uniform_buffers = num_uniform_buffers;
|
||||
|
||||
SDL_GPUShader* shader = SDL_CreateGPUShader(device, &info);
|
||||
if (!shader) {
|
||||
SDL_Log("GpuPipeline: shader '%s' failed: %s", entrypoint, SDL_GetError());
|
||||
}
|
||||
return shader;
|
||||
}
|
||||
35
source/gpu/gpu_pipeline.hpp
Normal file
35
source/gpu/gpu_pipeline.hpp
Normal file
@@ -0,0 +1,35 @@
|
||||
#pragma once
|
||||
|
||||
#include <SDL3/SDL_gpu.h>
|
||||
|
||||
// ============================================================================
|
||||
// GpuPipeline — Creates and owns the graphics pipelines used by the engine.
|
||||
//
|
||||
// sprite_pipeline_ : textured quads, alpha blending.
|
||||
// Vertex layout: GpuVertex (pos float2, uv float2, col float4).
|
||||
// postfx_pipeline_ : full-screen triangle, no vertex buffer, no blend.
|
||||
// Reads offscreen texture, writes to swapchain.
|
||||
// ============================================================================
|
||||
class GpuPipeline {
|
||||
public:
|
||||
// target_format: pass SDL_GetGPUSwapchainTextureFormat() result.
|
||||
// offscreen_format: format of the offscreen render target.
|
||||
bool init(SDL_GPUDevice* device,
|
||||
SDL_GPUTextureFormat target_format,
|
||||
SDL_GPUTextureFormat offscreen_format);
|
||||
void destroy(SDL_GPUDevice* device);
|
||||
|
||||
SDL_GPUGraphicsPipeline* spritePipeline() const { return sprite_pipeline_; }
|
||||
SDL_GPUGraphicsPipeline* postfxPipeline() const { return postfx_pipeline_; }
|
||||
|
||||
private:
|
||||
SDL_GPUShader* createShader(SDL_GPUDevice* device,
|
||||
const char* msl_source,
|
||||
const char* entrypoint,
|
||||
SDL_GPUShaderStage stage,
|
||||
Uint32 num_samplers,
|
||||
Uint32 num_uniform_buffers);
|
||||
|
||||
SDL_GPUGraphicsPipeline* sprite_pipeline_ = nullptr;
|
||||
SDL_GPUGraphicsPipeline* postfx_pipeline_ = nullptr;
|
||||
};
|
||||
192
source/gpu/gpu_sprite_batch.cpp
Normal file
192
source/gpu/gpu_sprite_batch.cpp
Normal file
@@ -0,0 +1,192 @@
|
||||
#include "gpu_sprite_batch.hpp"
|
||||
|
||||
#include <SDL3/SDL_log.h>
|
||||
#include <cstring> // memcpy
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Public interface
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
bool GpuSpriteBatch::init(SDL_GPUDevice* device) {
|
||||
// Pre-allocate GPU buffers large enough for MAX_SPRITES quads.
|
||||
Uint32 max_verts = static_cast<Uint32>(MAX_SPRITES) * 4;
|
||||
Uint32 max_indices = static_cast<Uint32>(MAX_SPRITES) * 6;
|
||||
|
||||
Uint32 vb_size = max_verts * sizeof(GpuVertex);
|
||||
Uint32 ib_size = max_indices * sizeof(uint32_t);
|
||||
|
||||
// Vertex buffer
|
||||
SDL_GPUBufferCreateInfo vb_info = {};
|
||||
vb_info.usage = SDL_GPU_BUFFERUSAGE_VERTEX;
|
||||
vb_info.size = vb_size;
|
||||
vertex_buf_ = SDL_CreateGPUBuffer(device, &vb_info);
|
||||
if (!vertex_buf_) {
|
||||
SDL_Log("GpuSpriteBatch: vertex buffer creation failed: %s", SDL_GetError());
|
||||
return false;
|
||||
}
|
||||
|
||||
// Index buffer
|
||||
SDL_GPUBufferCreateInfo ib_info = {};
|
||||
ib_info.usage = SDL_GPU_BUFFERUSAGE_INDEX;
|
||||
ib_info.size = ib_size;
|
||||
index_buf_ = SDL_CreateGPUBuffer(device, &ib_info);
|
||||
if (!index_buf_) {
|
||||
SDL_Log("GpuSpriteBatch: index buffer creation failed: %s", SDL_GetError());
|
||||
return false;
|
||||
}
|
||||
|
||||
// Transfer buffers (reused every frame via cycle=true on upload)
|
||||
SDL_GPUTransferBufferCreateInfo tb_info = {};
|
||||
tb_info.usage = SDL_GPU_TRANSFERBUFFERUSAGE_UPLOAD;
|
||||
|
||||
tb_info.size = vb_size;
|
||||
vertex_transfer_ = SDL_CreateGPUTransferBuffer(device, &tb_info);
|
||||
if (!vertex_transfer_) {
|
||||
SDL_Log("GpuSpriteBatch: vertex transfer buffer failed: %s", SDL_GetError());
|
||||
return false;
|
||||
}
|
||||
|
||||
tb_info.size = ib_size;
|
||||
index_transfer_ = SDL_CreateGPUTransferBuffer(device, &tb_info);
|
||||
if (!index_transfer_) {
|
||||
SDL_Log("GpuSpriteBatch: index transfer buffer failed: %s", SDL_GetError());
|
||||
return false;
|
||||
}
|
||||
|
||||
vertices_.reserve(MAX_SPRITES * 4);
|
||||
indices_.reserve(MAX_SPRITES * 6);
|
||||
return true;
|
||||
}
|
||||
|
||||
void GpuSpriteBatch::destroy(SDL_GPUDevice* device) {
|
||||
if (!device) return;
|
||||
if (vertex_transfer_) { SDL_ReleaseGPUTransferBuffer(device, vertex_transfer_); vertex_transfer_ = nullptr; }
|
||||
if (index_transfer_) { SDL_ReleaseGPUTransferBuffer(device, index_transfer_); index_transfer_ = nullptr; }
|
||||
if (vertex_buf_) { SDL_ReleaseGPUBuffer(device, vertex_buf_); vertex_buf_ = nullptr; }
|
||||
if (index_buf_) { SDL_ReleaseGPUBuffer(device, index_buf_); index_buf_ = nullptr; }
|
||||
}
|
||||
|
||||
void GpuSpriteBatch::beginFrame() {
|
||||
vertices_.clear();
|
||||
indices_.clear();
|
||||
bg_index_count_ = 0;
|
||||
sprite_index_offset_ = 0;
|
||||
sprite_index_count_ = 0;
|
||||
overlay_index_offset_ = 0;
|
||||
overlay_index_count_ = 0;
|
||||
}
|
||||
|
||||
void GpuSpriteBatch::addBackground(float screen_w, float screen_h,
|
||||
float top_r, float top_g, float top_b,
|
||||
float bot_r, float bot_g, float bot_b) {
|
||||
// Background is the full screen quad, corners:
|
||||
// TL(-1, 1) TR(1, 1) → top color
|
||||
// BL(-1,-1) BR(1,-1) → bottom color
|
||||
// We push it as 4 separate vertices (different colors per row).
|
||||
uint32_t vi = static_cast<uint32_t>(vertices_.size());
|
||||
|
||||
// Top-left
|
||||
vertices_.push_back({ -1.0f, 1.0f, 0.0f, 0.0f, top_r, top_g, top_b, 1.0f });
|
||||
// Top-right
|
||||
vertices_.push_back({ 1.0f, 1.0f, 1.0f, 0.0f, top_r, top_g, top_b, 1.0f });
|
||||
// Bottom-right
|
||||
vertices_.push_back({ 1.0f, -1.0f, 1.0f, 1.0f, bot_r, bot_g, bot_b, 1.0f });
|
||||
// Bottom-left
|
||||
vertices_.push_back({ -1.0f, -1.0f, 0.0f, 1.0f, bot_r, bot_g, bot_b, 1.0f });
|
||||
|
||||
// Two triangles: TL-TR-BR, BR-BL-TL
|
||||
indices_.push_back(vi + 0); indices_.push_back(vi + 1); indices_.push_back(vi + 2);
|
||||
indices_.push_back(vi + 2); indices_.push_back(vi + 3); indices_.push_back(vi + 0);
|
||||
|
||||
bg_index_count_ = 6;
|
||||
sprite_index_offset_ = 6;
|
||||
|
||||
(void)screen_w; (void)screen_h; // unused — bg always covers full NDC
|
||||
}
|
||||
|
||||
void GpuSpriteBatch::addSprite(float x, float y, float w, float h,
|
||||
float r, float g, float b, float a,
|
||||
float scale,
|
||||
float screen_w, float screen_h) {
|
||||
// Apply scale around the sprite centre
|
||||
float scaled_w = w * scale;
|
||||
float scaled_h = h * scale;
|
||||
float offset_x = (w - scaled_w) * 0.5f;
|
||||
float offset_y = (h - scaled_h) * 0.5f;
|
||||
|
||||
float px0 = x + offset_x;
|
||||
float py0 = y + offset_y;
|
||||
float px1 = px0 + scaled_w;
|
||||
float py1 = py0 + scaled_h;
|
||||
|
||||
float ndx0, ndy0, ndx1, ndy1;
|
||||
toNDC(px0, py0, screen_w, screen_h, ndx0, ndy0);
|
||||
toNDC(px1, py1, screen_w, screen_h, ndx1, ndy1);
|
||||
|
||||
pushQuad(ndx0, ndy0, ndx1, ndy1, 0.0f, 0.0f, 1.0f, 1.0f, r, g, b, a);
|
||||
sprite_index_count_ += 6;
|
||||
}
|
||||
|
||||
void GpuSpriteBatch::addFullscreenOverlay() {
|
||||
overlay_index_offset_ = static_cast<int>(indices_.size());
|
||||
pushQuad(-1.0f, 1.0f, 1.0f, -1.0f, 0.0f, 0.0f, 1.0f, 1.0f, 1.0f, 1.0f, 1.0f, 1.0f);
|
||||
overlay_index_count_ = 6;
|
||||
}
|
||||
|
||||
bool GpuSpriteBatch::uploadBatch(SDL_GPUDevice* device, SDL_GPUCommandBuffer* cmd_buf) {
|
||||
if (vertices_.empty()) return false;
|
||||
|
||||
Uint32 vb_size = static_cast<Uint32>(vertices_.size() * sizeof(GpuVertex));
|
||||
Uint32 ib_size = static_cast<Uint32>(indices_.size() * sizeof(uint32_t));
|
||||
|
||||
// Map → write → unmap transfer buffers
|
||||
void* vp = SDL_MapGPUTransferBuffer(device, vertex_transfer_, true /* cycle */);
|
||||
if (!vp) { SDL_Log("GpuSpriteBatch: vertex map failed"); return false; }
|
||||
memcpy(vp, vertices_.data(), vb_size);
|
||||
SDL_UnmapGPUTransferBuffer(device, vertex_transfer_);
|
||||
|
||||
void* ip = SDL_MapGPUTransferBuffer(device, index_transfer_, true /* cycle */);
|
||||
if (!ip) { SDL_Log("GpuSpriteBatch: index map failed"); return false; }
|
||||
memcpy(ip, indices_.data(), ib_size);
|
||||
SDL_UnmapGPUTransferBuffer(device, index_transfer_);
|
||||
|
||||
// Upload via copy pass
|
||||
SDL_GPUCopyPass* copy = SDL_BeginGPUCopyPass(cmd_buf);
|
||||
|
||||
SDL_GPUTransferBufferLocation v_src = { vertex_transfer_, 0 };
|
||||
SDL_GPUBufferRegion v_dst = { vertex_buf_, 0, vb_size };
|
||||
SDL_UploadToGPUBuffer(copy, &v_src, &v_dst, true /* cycle */);
|
||||
|
||||
SDL_GPUTransferBufferLocation i_src = { index_transfer_, 0 };
|
||||
SDL_GPUBufferRegion i_dst = { index_buf_, 0, ib_size };
|
||||
SDL_UploadToGPUBuffer(copy, &i_src, &i_dst, true /* cycle */);
|
||||
|
||||
SDL_EndGPUCopyPass(copy);
|
||||
return true;
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Private helpers
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
void GpuSpriteBatch::toNDC(float px, float py,
|
||||
float screen_w, float screen_h,
|
||||
float& ndx, float& ndy) const {
|
||||
ndx = (px / screen_w) * 2.0f - 1.0f;
|
||||
ndy = 1.0f - (py / screen_h) * 2.0f;
|
||||
}
|
||||
|
||||
void GpuSpriteBatch::pushQuad(float ndx0, float ndy0, float ndx1, float ndy1,
|
||||
float u0, float v0, float u1, float v1,
|
||||
float r, float g, float b, float a) {
|
||||
uint32_t vi = static_cast<uint32_t>(vertices_.size());
|
||||
|
||||
// TL, TR, BR, BL
|
||||
vertices_.push_back({ ndx0, ndy0, u0, v0, r, g, b, a });
|
||||
vertices_.push_back({ ndx1, ndy0, u1, v0, r, g, b, a });
|
||||
vertices_.push_back({ ndx1, ndy1, u1, v1, r, g, b, a });
|
||||
vertices_.push_back({ ndx0, ndy1, u0, v1, r, g, b, a });
|
||||
|
||||
indices_.push_back(vi + 0); indices_.push_back(vi + 1); indices_.push_back(vi + 2);
|
||||
indices_.push_back(vi + 2); indices_.push_back(vi + 3); indices_.push_back(vi + 0);
|
||||
}
|
||||
86
source/gpu/gpu_sprite_batch.hpp
Normal file
86
source/gpu/gpu_sprite_batch.hpp
Normal file
@@ -0,0 +1,86 @@
|
||||
#pragma once
|
||||
|
||||
#include <SDL3/SDL_gpu.h>
|
||||
#include <vector>
|
||||
#include <cstdint>
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// GpuVertex — 8-float vertex layout sent to the GPU.
|
||||
// Position is in NDC (pre-transformed on CPU), UV in [0,1], color in [0,1].
|
||||
// ---------------------------------------------------------------------------
|
||||
struct GpuVertex {
|
||||
float x, y; // NDC position (−1..1)
|
||||
float u, v; // Texture coords (0..1)
|
||||
float r, g, b, a; // RGBA color (0..1)
|
||||
};
|
||||
|
||||
// ============================================================================
|
||||
// GpuSpriteBatch — Accumulates sprite quads, uploads them in one copy pass.
|
||||
//
|
||||
// Usage per frame:
|
||||
// batch.beginFrame();
|
||||
// batch.addBackground(...); // Must be first (bg indices = [0..5])
|
||||
// batch.addSprite(...) × N;
|
||||
// batch.uploadBatch(device, cmd); // Copy pass
|
||||
// // Then in render pass: bind buffers, draw bg with white tex, draw sprites.
|
||||
// ============================================================================
|
||||
class GpuSpriteBatch {
|
||||
public:
|
||||
// Maximum sprites (background + UI overlay each count as one sprite)
|
||||
static constexpr int MAX_SPRITES = 65536;
|
||||
|
||||
bool init(SDL_GPUDevice* device);
|
||||
void destroy(SDL_GPUDevice* device);
|
||||
|
||||
void beginFrame();
|
||||
|
||||
// Add the full-screen background gradient quad.
|
||||
// top_* and bot_* are RGB in [0,1].
|
||||
void addBackground(float screen_w, float screen_h,
|
||||
float top_r, float top_g, float top_b,
|
||||
float bot_r, float bot_g, float bot_b);
|
||||
|
||||
// Add a sprite quad (pixel coordinates).
|
||||
// scale: uniform scale around the quad centre.
|
||||
void addSprite(float x, float y, float w, float h,
|
||||
float r, float g, float b, float a,
|
||||
float scale,
|
||||
float screen_w, float screen_h);
|
||||
|
||||
// Add a full-screen overlay quad (e.g. UI surface, NDC −1..1).
|
||||
void addFullscreenOverlay();
|
||||
|
||||
// Upload CPU vectors to GPU buffers via a copy pass.
|
||||
// Returns false if the batch is empty.
|
||||
bool uploadBatch(SDL_GPUDevice* device, SDL_GPUCommandBuffer* cmd_buf);
|
||||
|
||||
SDL_GPUBuffer* vertexBuffer() const { return vertex_buf_; }
|
||||
SDL_GPUBuffer* indexBuffer() const { return index_buf_; }
|
||||
int bgIndexCount() const { return bg_index_count_; }
|
||||
int overlayIndexOffset() const { return overlay_index_offset_; }
|
||||
int overlayIndexCount() const { return overlay_index_count_; }
|
||||
int spriteIndexOffset() const { return sprite_index_offset_; }
|
||||
int spriteIndexCount() const { return sprite_index_count_; }
|
||||
bool isEmpty() const { return vertices_.empty(); }
|
||||
|
||||
private:
|
||||
void toNDC(float px, float py, float screen_w, float screen_h,
|
||||
float& ndx, float& ndy) const;
|
||||
void pushQuad(float ndx0, float ndy0, float ndx1, float ndy1,
|
||||
float u0, float v0, float u1, float v1,
|
||||
float r, float g, float b, float a);
|
||||
|
||||
std::vector<GpuVertex> vertices_;
|
||||
std::vector<uint32_t> indices_;
|
||||
|
||||
SDL_GPUBuffer* vertex_buf_ = nullptr;
|
||||
SDL_GPUBuffer* index_buf_ = nullptr;
|
||||
SDL_GPUTransferBuffer* vertex_transfer_ = nullptr;
|
||||
SDL_GPUTransferBuffer* index_transfer_ = nullptr;
|
||||
|
||||
int bg_index_count_ = 0;
|
||||
int sprite_index_offset_ = 0;
|
||||
int sprite_index_count_ = 0;
|
||||
int overlay_index_offset_ = 0;
|
||||
int overlay_index_count_ = 0;
|
||||
};
|
||||
208
source/gpu/gpu_texture.cpp
Normal file
208
source/gpu/gpu_texture.cpp
Normal file
@@ -0,0 +1,208 @@
|
||||
#include "gpu_texture.hpp"
|
||||
|
||||
#include <SDL3/SDL_log.h>
|
||||
#include <SDL3/SDL_pixels.h>
|
||||
#include <cstring> // memcpy
|
||||
#include <string>
|
||||
|
||||
// stb_image is compiled in texture.cpp (STB_IMAGE_IMPLEMENTATION defined there)
|
||||
#include "external/stb_image.h"
|
||||
#include "resource_manager.hpp"
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Public interface
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
bool GpuTexture::fromFile(SDL_GPUDevice* device, const std::string& file_path) {
|
||||
unsigned char* resource_data = nullptr;
|
||||
size_t resource_size = 0;
|
||||
|
||||
if (!ResourceManager::loadResource(file_path, resource_data, resource_size)) {
|
||||
SDL_Log("GpuTexture: can't load resource '%s'", file_path.c_str());
|
||||
return false;
|
||||
}
|
||||
|
||||
int w = 0, h = 0, orig = 0;
|
||||
unsigned char* pixels = stbi_load_from_memory(
|
||||
resource_data, static_cast<int>(resource_size),
|
||||
&w, &h, &orig, STBI_rgb_alpha);
|
||||
delete[] resource_data;
|
||||
|
||||
if (!pixels) {
|
||||
SDL_Log("GpuTexture: stbi decode failed for '%s': %s",
|
||||
file_path.c_str(), stbi_failure_reason());
|
||||
return false;
|
||||
}
|
||||
|
||||
destroy(device);
|
||||
bool ok = uploadPixels(device, pixels, w, h, SDL_GPU_TEXTUREFORMAT_R8G8B8A8_UNORM);
|
||||
stbi_image_free(pixels);
|
||||
|
||||
if (ok) {
|
||||
ok = createSampler(device, true /*nearest = pixel-perfect sprites*/);
|
||||
}
|
||||
return ok;
|
||||
}
|
||||
|
||||
bool GpuTexture::fromSurface(SDL_GPUDevice* device, SDL_Surface* surface, bool nearest) {
|
||||
if (!surface) return false;
|
||||
|
||||
// Ensure RGBA32 format
|
||||
SDL_Surface* rgba = surface;
|
||||
bool need_free = false;
|
||||
if (surface->format != SDL_PIXELFORMAT_RGBA32) {
|
||||
rgba = SDL_ConvertSurface(surface, SDL_PIXELFORMAT_RGBA32);
|
||||
if (!rgba) {
|
||||
SDL_Log("GpuTexture: SDL_ConvertSurface failed: %s", SDL_GetError());
|
||||
return false;
|
||||
}
|
||||
need_free = true;
|
||||
}
|
||||
|
||||
destroy(device);
|
||||
bool ok = uploadPixels(device, rgba->pixels, rgba->w, rgba->h,
|
||||
SDL_GPU_TEXTUREFORMAT_R8G8B8A8_UNORM);
|
||||
if (ok) ok = createSampler(device, nearest);
|
||||
|
||||
if (need_free) SDL_DestroySurface(rgba);
|
||||
return ok;
|
||||
}
|
||||
|
||||
bool GpuTexture::createRenderTarget(SDL_GPUDevice* device, int w, int h,
|
||||
SDL_GPUTextureFormat format) {
|
||||
destroy(device);
|
||||
|
||||
SDL_GPUTextureCreateInfo info = {};
|
||||
info.type = SDL_GPU_TEXTURETYPE_2D;
|
||||
info.format = format;
|
||||
info.usage = SDL_GPU_TEXTUREUSAGE_COLOR_TARGET
|
||||
| SDL_GPU_TEXTUREUSAGE_SAMPLER;
|
||||
info.width = static_cast<Uint32>(w);
|
||||
info.height = static_cast<Uint32>(h);
|
||||
info.layer_count_or_depth = 1;
|
||||
info.num_levels = 1;
|
||||
info.sample_count = SDL_GPU_SAMPLECOUNT_1;
|
||||
|
||||
texture_ = SDL_CreateGPUTexture(device, &info);
|
||||
if (!texture_) {
|
||||
SDL_Log("GpuTexture: createRenderTarget failed: %s", SDL_GetError());
|
||||
return false;
|
||||
}
|
||||
width_ = w;
|
||||
height_ = h;
|
||||
|
||||
// Render targets are sampled with linear filter (postfx reads them)
|
||||
return createSampler(device, false);
|
||||
}
|
||||
|
||||
bool GpuTexture::createWhite(SDL_GPUDevice* device) {
|
||||
destroy(device);
|
||||
// 1×1 white RGBA pixel
|
||||
const Uint8 white[4] = {255, 255, 255, 255};
|
||||
bool ok = uploadPixels(device, white, 1, 1,
|
||||
SDL_GPU_TEXTUREFORMAT_R8G8B8A8_UNORM);
|
||||
if (ok) ok = createSampler(device, true);
|
||||
return ok;
|
||||
}
|
||||
|
||||
void GpuTexture::destroy(SDL_GPUDevice* device) {
|
||||
if (!device) return;
|
||||
if (sampler_) { SDL_ReleaseGPUSampler(device, sampler_); sampler_ = nullptr; }
|
||||
if (texture_) { SDL_ReleaseGPUTexture(device, texture_); texture_ = nullptr; }
|
||||
width_ = height_ = 0;
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Private helpers
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
bool GpuTexture::uploadPixels(SDL_GPUDevice* device, const void* pixels,
|
||||
int w, int h, SDL_GPUTextureFormat format) {
|
||||
// Create GPU texture
|
||||
SDL_GPUTextureCreateInfo tex_info = {};
|
||||
tex_info.type = SDL_GPU_TEXTURETYPE_2D;
|
||||
tex_info.format = format;
|
||||
tex_info.usage = SDL_GPU_TEXTUREUSAGE_SAMPLER;
|
||||
tex_info.width = static_cast<Uint32>(w);
|
||||
tex_info.height = static_cast<Uint32>(h);
|
||||
tex_info.layer_count_or_depth = 1;
|
||||
tex_info.num_levels = 1;
|
||||
tex_info.sample_count = SDL_GPU_SAMPLECOUNT_1;
|
||||
|
||||
texture_ = SDL_CreateGPUTexture(device, &tex_info);
|
||||
if (!texture_) {
|
||||
SDL_Log("GpuTexture: SDL_CreateGPUTexture failed: %s", SDL_GetError());
|
||||
return false;
|
||||
}
|
||||
|
||||
// Create transfer buffer and upload pixels
|
||||
Uint32 data_size = static_cast<Uint32>(w * h * 4); // RGBA = 4 bytes/pixel
|
||||
|
||||
SDL_GPUTransferBufferCreateInfo tb_info = {};
|
||||
tb_info.usage = SDL_GPU_TRANSFERBUFFERUSAGE_UPLOAD;
|
||||
tb_info.size = data_size;
|
||||
|
||||
SDL_GPUTransferBuffer* transfer = SDL_CreateGPUTransferBuffer(device, &tb_info);
|
||||
if (!transfer) {
|
||||
SDL_Log("GpuTexture: transfer buffer creation failed: %s", SDL_GetError());
|
||||
SDL_ReleaseGPUTexture(device, texture_);
|
||||
texture_ = nullptr;
|
||||
return false;
|
||||
}
|
||||
|
||||
void* mapped = SDL_MapGPUTransferBuffer(device, transfer, false);
|
||||
if (!mapped) {
|
||||
SDL_Log("GpuTexture: map failed: %s", SDL_GetError());
|
||||
SDL_ReleaseGPUTransferBuffer(device, transfer);
|
||||
SDL_ReleaseGPUTexture(device, texture_);
|
||||
texture_ = nullptr;
|
||||
return false;
|
||||
}
|
||||
memcpy(mapped, pixels, data_size);
|
||||
SDL_UnmapGPUTransferBuffer(device, transfer);
|
||||
|
||||
// Upload via command buffer
|
||||
SDL_GPUCommandBuffer* cmd = SDL_AcquireGPUCommandBuffer(device);
|
||||
SDL_GPUCopyPass* copy = SDL_BeginGPUCopyPass(cmd);
|
||||
|
||||
SDL_GPUTextureTransferInfo src = {};
|
||||
src.transfer_buffer = transfer;
|
||||
src.offset = 0;
|
||||
src.pixels_per_row = static_cast<Uint32>(w);
|
||||
src.rows_per_layer = static_cast<Uint32>(h);
|
||||
|
||||
SDL_GPUTextureRegion dst = {};
|
||||
dst.texture = texture_;
|
||||
dst.mip_level = 0;
|
||||
dst.layer = 0;
|
||||
dst.x = dst.y = dst.z = 0;
|
||||
dst.w = static_cast<Uint32>(w);
|
||||
dst.h = static_cast<Uint32>(h);
|
||||
dst.d = 1;
|
||||
|
||||
SDL_UploadToGPUTexture(copy, &src, &dst, false);
|
||||
SDL_EndGPUCopyPass(copy);
|
||||
SDL_SubmitGPUCommandBuffer(cmd);
|
||||
|
||||
SDL_ReleaseGPUTransferBuffer(device, transfer);
|
||||
width_ = w;
|
||||
height_ = h;
|
||||
return true;
|
||||
}
|
||||
|
||||
bool GpuTexture::createSampler(SDL_GPUDevice* device, bool nearest) {
|
||||
SDL_GPUSamplerCreateInfo info = {};
|
||||
info.min_filter = nearest ? SDL_GPU_FILTER_NEAREST : SDL_GPU_FILTER_LINEAR;
|
||||
info.mag_filter = nearest ? SDL_GPU_FILTER_NEAREST : SDL_GPU_FILTER_LINEAR;
|
||||
info.mipmap_mode = SDL_GPU_SAMPLERMIPMAPMODE_NEAREST;
|
||||
info.address_mode_u = SDL_GPU_SAMPLERADDRESSMODE_CLAMP_TO_EDGE;
|
||||
info.address_mode_v = SDL_GPU_SAMPLERADDRESSMODE_CLAMP_TO_EDGE;
|
||||
info.address_mode_w = SDL_GPU_SAMPLERADDRESSMODE_CLAMP_TO_EDGE;
|
||||
|
||||
sampler_ = SDL_CreateGPUSampler(device, &info);
|
||||
if (!sampler_) {
|
||||
SDL_Log("GpuTexture: SDL_CreateGPUSampler failed: %s", SDL_GetError());
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
48
source/gpu/gpu_texture.hpp
Normal file
48
source/gpu/gpu_texture.hpp
Normal file
@@ -0,0 +1,48 @@
|
||||
#pragma once
|
||||
|
||||
#include <SDL3/SDL_gpu.h>
|
||||
#include <SDL3/SDL_surface.h>
|
||||
#include <string>
|
||||
|
||||
// ============================================================================
|
||||
// GpuTexture — SDL_GPU texture + sampler wrapper
|
||||
// Handles sprite textures, render targets, and the 1×1 white utility texture.
|
||||
// ============================================================================
|
||||
class GpuTexture {
|
||||
public:
|
||||
GpuTexture() = default;
|
||||
~GpuTexture() = default;
|
||||
|
||||
// Load from resource path (pack or disk) using stb_image.
|
||||
bool fromFile(SDL_GPUDevice* device, const std::string& file_path);
|
||||
|
||||
// Upload pixel data from an SDL_Surface to a new GPU texture + sampler.
|
||||
// Uses nearest-neighbor filter for sprite pixel-perfect look.
|
||||
bool fromSurface(SDL_GPUDevice* device, SDL_Surface* surface, bool nearest = true);
|
||||
|
||||
// Create an offscreen render target (COLOR_TARGET | SAMPLER usage).
|
||||
bool createRenderTarget(SDL_GPUDevice* device, int w, int h,
|
||||
SDL_GPUTextureFormat format);
|
||||
|
||||
// Create a 1×1 opaque white texture (used for untextured geometry).
|
||||
bool createWhite(SDL_GPUDevice* device);
|
||||
|
||||
// Release GPU resources.
|
||||
void destroy(SDL_GPUDevice* device);
|
||||
|
||||
SDL_GPUTexture* texture() const { return texture_; }
|
||||
SDL_GPUSampler* sampler() const { return sampler_; }
|
||||
int width() const { return width_; }
|
||||
int height() const { return height_; }
|
||||
bool isValid() const { return texture_ != nullptr; }
|
||||
|
||||
private:
|
||||
bool uploadPixels(SDL_GPUDevice* device, const void* pixels,
|
||||
int w, int h, SDL_GPUTextureFormat format);
|
||||
bool createSampler(SDL_GPUDevice* device, bool nearest);
|
||||
|
||||
SDL_GPUTexture* texture_ = nullptr;
|
||||
SDL_GPUSampler* sampler_ = nullptr;
|
||||
int width_ = 0;
|
||||
int height_ = 0;
|
||||
};
|
||||
Reference in New Issue
Block a user