feat(captures): captura de pantalla amb F9 (PNG amb shaders, a mida de finestra)
This commit is contained in:
@@ -7,6 +7,7 @@
|
||||
|
||||
#include <algorithm>
|
||||
#include <cmath>
|
||||
#include <cstddef>
|
||||
#include <cstdint>
|
||||
#include <cstring>
|
||||
#include <iostream>
|
||||
@@ -132,6 +133,7 @@ namespace Rendering::GPU {
|
||||
}
|
||||
|
||||
void GpuFrameRenderer::destroy() {
|
||||
destroyCaptureResources();
|
||||
destroyOffscreen();
|
||||
postfx_pipeline_.destroy();
|
||||
bloom_pipeline_.destroy();
|
||||
@@ -172,7 +174,7 @@ namespace Rendering::GPU {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!SDL_WaitAndAcquireGPUSwapchainTexture(cmd_buffer_, device_.window(), &swapchain_texture_, nullptr, nullptr)) {
|
||||
if (!SDL_WaitAndAcquireGPUSwapchainTexture(cmd_buffer_, device_.window(), &swapchain_texture_, &swapchain_w_, &swapchain_h_)) {
|
||||
std::cerr << "[GpuFrameRenderer] WaitAndAcquire: " << SDL_GetError() << '\n';
|
||||
SDL_SubmitGPUCommandBuffer(cmd_buffer_);
|
||||
cmd_buffer_ = nullptr;
|
||||
@@ -540,11 +542,20 @@ namespace Rendering::GPU {
|
||||
SDL_EndGPURenderPass(render_pass_);
|
||||
render_pass_ = nullptr;
|
||||
}
|
||||
compositeTo(swapchain_texture_);
|
||||
}
|
||||
|
||||
// Pase final: render pass sobre SWAPCHAIN con clear a negro (cubre el
|
||||
void GpuFrameRenderer::compositeTo(SDL_GPUTexture* target_tex) {
|
||||
// Cos comú del pase de postpro: sample offscreen + bloom → target_tex
|
||||
// (swapchain en el camí normal, o la textura de captura per al
|
||||
// screenshot). El caller s'encarrega de tancar qualsevol render pass
|
||||
// previ. Com que la textura de captura té la mateixa mida que la
|
||||
// swapchain, el mateix viewport/letterbox produeix píxels idèntics.
|
||||
|
||||
// Pase final: render pass sobre el target con clear a negro (cubre el
|
||||
// letterbox del viewport físico).
|
||||
SDL_GPUColorTargetInfo target{};
|
||||
target.texture = swapchain_texture_;
|
||||
target.texture = target_tex;
|
||||
target.clear_color = SDL_FColor{.r = 0.0F, .g = 0.0F, .b = 0.0F, .a = 1.0F};
|
||||
target.load_op = SDL_GPU_LOADOP_CLEAR;
|
||||
target.store_op = SDL_GPU_STOREOP_STORE;
|
||||
@@ -664,14 +675,150 @@ namespace Rendering::GPU {
|
||||
}
|
||||
flushBatch();
|
||||
bloomPass();
|
||||
compositePass();
|
||||
compositePass(); // → swapchain (camí de presentació normal)
|
||||
if (render_pass_ != nullptr) {
|
||||
SDL_EndGPURenderPass(render_pass_);
|
||||
render_pass_ = nullptr;
|
||||
}
|
||||
SDL_SubmitGPUCommandBuffer(cmd_buffer_);
|
||||
cmd_buffer_ = nullptr;
|
||||
|
||||
// Captura (screenshot): segon composite sobre la textura pròpia +
|
||||
// readback a CPU. Només quan hi ha petició pendent i els recursos es
|
||||
// poden crear; si no, submit normal i prou. La petició es consumeix
|
||||
// sempre (s'haja pogut capturar o no) per no quedar enganxada.
|
||||
if (capture_requested_ && ensureCaptureResources()) {
|
||||
compositeTo(capture_texture_);
|
||||
if (render_pass_ != nullptr) {
|
||||
SDL_EndGPURenderPass(render_pass_);
|
||||
render_pass_ = nullptr;
|
||||
}
|
||||
downloadCapture(); // fa submit + fence + map; deixa cmd_buffer_ a nullptr
|
||||
} else {
|
||||
SDL_SubmitGPUCommandBuffer(cmd_buffer_);
|
||||
cmd_buffer_ = nullptr;
|
||||
}
|
||||
capture_requested_ = false;
|
||||
swapchain_texture_ = nullptr;
|
||||
}
|
||||
|
||||
auto GpuFrameRenderer::ensureCaptureResources() -> bool {
|
||||
SDL_GPUDevice* dev = device_.get();
|
||||
if (dev == nullptr || swapchain_w_ == 0 || swapchain_h_ == 0) {
|
||||
return false;
|
||||
}
|
||||
const int W = static_cast<int>(swapchain_w_);
|
||||
const int H = static_cast<int>(swapchain_h_);
|
||||
if (capture_texture_ != nullptr && capture_tex_w_ == W && capture_tex_h_ == H) {
|
||||
return true; // ja són de la mida correcta
|
||||
}
|
||||
destroyCaptureResources();
|
||||
|
||||
// Mateix format que la swapchain: és el target del postfx_pipeline_.
|
||||
SDL_GPUTextureCreateInfo tex_info{};
|
||||
tex_info.type = SDL_GPU_TEXTURETYPE_2D;
|
||||
tex_info.format = device_.swapchainFormat();
|
||||
tex_info.usage = SDL_GPU_TEXTUREUSAGE_COLOR_TARGET;
|
||||
tex_info.width = swapchain_w_;
|
||||
tex_info.height = swapchain_h_;
|
||||
tex_info.layer_count_or_depth = 1;
|
||||
tex_info.num_levels = 1;
|
||||
tex_info.sample_count = SDL_GPU_SAMPLECOUNT_1;
|
||||
capture_texture_ = SDL_CreateGPUTexture(dev, &tex_info);
|
||||
if (capture_texture_ == nullptr) {
|
||||
std::cerr << "[GpuFrameRenderer] CreateGPUTexture (captura): " << SDL_GetError() << '\n';
|
||||
return false;
|
||||
}
|
||||
|
||||
SDL_GPUTransferBufferCreateInfo tbo_info{};
|
||||
tbo_info.usage = SDL_GPU_TRANSFERBUFFERUSAGE_DOWNLOAD;
|
||||
tbo_info.size = static_cast<uint32_t>(W * H * 4);
|
||||
capture_transfer_ = SDL_CreateGPUTransferBuffer(dev, &tbo_info);
|
||||
if (capture_transfer_ == nullptr) {
|
||||
std::cerr << "[GpuFrameRenderer] CreateGPUTransferBuffer (captura): " << SDL_GetError() << '\n';
|
||||
SDL_ReleaseGPUTexture(dev, capture_texture_);
|
||||
capture_texture_ = nullptr;
|
||||
return false;
|
||||
}
|
||||
capture_tex_w_ = W;
|
||||
capture_tex_h_ = H;
|
||||
return true;
|
||||
}
|
||||
|
||||
void GpuFrameRenderer::downloadCapture() {
|
||||
SDL_GPUDevice* dev = device_.get();
|
||||
|
||||
// Copy pass: descarrega la textura de captura al transfer buffer.
|
||||
SDL_GPUCopyPass* copy_pass = SDL_BeginGPUCopyPass(cmd_buffer_);
|
||||
SDL_GPUTextureRegion src{};
|
||||
src.texture = capture_texture_;
|
||||
src.w = swapchain_w_;
|
||||
src.h = swapchain_h_;
|
||||
src.d = 1;
|
||||
SDL_GPUTextureTransferInfo dst{};
|
||||
dst.transfer_buffer = capture_transfer_;
|
||||
dst.offset = 0;
|
||||
dst.pixels_per_row = swapchain_w_;
|
||||
dst.rows_per_layer = swapchain_h_;
|
||||
SDL_DownloadFromGPUTexture(copy_pass, &src, &dst);
|
||||
SDL_EndGPUCopyPass(copy_pass);
|
||||
|
||||
// Submit amb fence i esperar: el readback ha d'estar complet abans de mapar.
|
||||
SDL_GPUFence* fence = SDL_SubmitGPUCommandBufferAndAcquireFence(cmd_buffer_);
|
||||
cmd_buffer_ = nullptr;
|
||||
if (fence == nullptr) {
|
||||
std::cerr << "[GpuFrameRenderer] SubmitAndAcquireFence (captura): " << SDL_GetError() << '\n';
|
||||
return;
|
||||
}
|
||||
SDL_WaitForGPUFences(dev, true, &fence, 1);
|
||||
SDL_ReleaseGPUFence(dev, fence);
|
||||
|
||||
const auto* mapped = static_cast<const uint8_t*>(SDL_MapGPUTransferBuffer(dev, capture_transfer_, false));
|
||||
if (mapped == nullptr) {
|
||||
std::cerr << "[GpuFrameRenderer] MapGPUTransferBuffer (captura): " << SDL_GetError() << '\n';
|
||||
return;
|
||||
}
|
||||
|
||||
// Conversió a ARGB8888 (0xAARRGGBB), que és el que espera Screenshot::save.
|
||||
// El swapchain a Linux/Vulkan sol ser B8G8R8A8_UNORM (bytes B,G,R,A);
|
||||
// altrament tractem com R8G8B8A8_UNORM (bytes R,G,B,A). Alpha forçat a 255
|
||||
// perquè el composite ja escriu opac.
|
||||
const int W = static_cast<int>(swapchain_w_);
|
||||
const int H = static_cast<int>(swapchain_h_);
|
||||
const auto COUNT = static_cast<std::size_t>(W) * static_cast<std::size_t>(H);
|
||||
capture_pixels_.resize(COUNT);
|
||||
const bool BGRA = device_.swapchainFormat() == SDL_GPU_TEXTUREFORMAT_B8G8R8A8_UNORM;
|
||||
for (std::size_t i = 0; i < COUNT; ++i) {
|
||||
const uint8_t* px = mapped + (i * 4);
|
||||
const std::uint32_t R = BGRA ? px[2] : px[0];
|
||||
const std::uint32_t G = px[1];
|
||||
const std::uint32_t B = BGRA ? px[0] : px[2];
|
||||
capture_pixels_[i] = (0xFFU << 24) | (R << 16) | (G << 8) | B;
|
||||
}
|
||||
SDL_UnmapGPUTransferBuffer(dev, capture_transfer_);
|
||||
|
||||
capture_w_ = W;
|
||||
capture_h_ = H;
|
||||
capture_ready_ = true;
|
||||
}
|
||||
|
||||
void GpuFrameRenderer::destroyCaptureResources() {
|
||||
SDL_GPUDevice* dev = device_.get();
|
||||
if (dev == nullptr) {
|
||||
capture_texture_ = nullptr;
|
||||
capture_transfer_ = nullptr;
|
||||
capture_tex_w_ = 0;
|
||||
capture_tex_h_ = 0;
|
||||
return;
|
||||
}
|
||||
if (capture_texture_ != nullptr) {
|
||||
SDL_ReleaseGPUTexture(dev, capture_texture_);
|
||||
capture_texture_ = nullptr;
|
||||
}
|
||||
if (capture_transfer_ != nullptr) {
|
||||
SDL_ReleaseGPUTransferBuffer(dev, capture_transfer_);
|
||||
capture_transfer_ = nullptr;
|
||||
}
|
||||
capture_tex_w_ = 0;
|
||||
capture_tex_h_ = 0;
|
||||
}
|
||||
|
||||
} // namespace Rendering::GPU
|
||||
|
||||
@@ -133,6 +133,19 @@ namespace Rendering::GPU {
|
||||
void setPostFxEnabled(bool enabled) { postfx_enabled_ = enabled; }
|
||||
[[nodiscard]] auto isPostFxEnabled() const -> bool { return postfx_enabled_; }
|
||||
|
||||
// === Captura de pantalla (screenshot) ===
|
||||
// Demana una captura del frame actual: el pròxim endFrame fa un segon
|
||||
// composite sobre una textura pròpia (mida swapchain) i el descarrega a
|
||||
// CPU en ARGB8888. Cost zero quan no hi ha cap petició pendent. Després
|
||||
// del present, el caller comprova hasCapture(), llig captureData() i
|
||||
// crida clearCapture().
|
||||
void requestCapture() { capture_requested_ = true; }
|
||||
[[nodiscard]] auto hasCapture() const -> bool { return capture_ready_; }
|
||||
[[nodiscard]] auto captureData() const -> const std::uint32_t* { return capture_pixels_.data(); }
|
||||
[[nodiscard]] auto captureWidth() const -> int { return capture_w_; }
|
||||
[[nodiscard]] auto captureHeight() const -> int { return capture_h_; }
|
||||
void clearCapture() { capture_ready_ = false; }
|
||||
|
||||
// Acceso a internals.
|
||||
[[nodiscard]] auto device() -> GpuDevice& { return device_; }
|
||||
[[nodiscard]] auto isInsideFrame() const -> bool { return cmd_buffer_ != nullptr; }
|
||||
@@ -173,6 +186,26 @@ namespace Rendering::GPU {
|
||||
SDL_GPUTexture* bloom_texture_a_{nullptr};
|
||||
SDL_GPUTexture* bloom_texture_b_{nullptr};
|
||||
|
||||
// === Captura de pantalla ===
|
||||
// Dimensions reals de la swapchain (capturades a beginFrame). El target
|
||||
// de captura es crea a aquesta mida perquè el PNG surti exactament com
|
||||
// es veu en pantalla (amb letterbox), no a la resolució del offscreen.
|
||||
uint32_t swapchain_w_{0};
|
||||
uint32_t swapchain_h_{0};
|
||||
// Textura on es fa el segon composite (mateix format que la swapchain) i
|
||||
// transfer buffer per a descarregar-la a CPU. Es recreen si canvia la
|
||||
// mida de la finestra (capture_tex_w_/h_ = mida amb què es van crear).
|
||||
SDL_GPUTexture* capture_texture_{nullptr};
|
||||
SDL_GPUTransferBuffer* capture_transfer_{nullptr};
|
||||
int capture_tex_w_{0};
|
||||
int capture_tex_h_{0};
|
||||
// Píxels descarregats (ARGB8888, 0xAARRGGBB) i estat de la petició.
|
||||
std::vector<std::uint32_t> capture_pixels_;
|
||||
int capture_w_{0};
|
||||
int capture_h_{0};
|
||||
bool capture_requested_{false};
|
||||
bool capture_ready_{false};
|
||||
|
||||
// Batch del frame en curso.
|
||||
std::vector<LineVertex> vertices_;
|
||||
std::vector<uint16_t> indices_;
|
||||
@@ -202,8 +235,14 @@ namespace Rendering::GPU {
|
||||
void flushBatch();
|
||||
void bloomPass(); // pre-composite: H + V passes sobre les bloom textures
|
||||
void compositePass();
|
||||
void compositeTo(SDL_GPUTexture* target_tex); // cos comú del pase de postpro
|
||||
void applyFinalViewport();
|
||||
void applyCurrentScissor(); // re-aplica el top de clip_stack_ al render_pass_
|
||||
|
||||
// Captura: (re)crea recursos a mida swapchain, descarrega a CPU i allibera.
|
||||
[[nodiscard]] auto ensureCaptureResources() -> bool;
|
||||
void downloadCapture(); // copy pass + fence + map → capture_pixels_
|
||||
void destroyCaptureResources();
|
||||
};
|
||||
|
||||
} // namespace Rendering::GPU
|
||||
|
||||
Reference in New Issue
Block a user