The acrylic effect blits its backdrop from the default framebuffer using the logical viewport size as the source rectangle. On a HiDPI display the framebuffer is larger than the logical size, so the blur sampled only the lower-left corner of the screen and the glass showed the wrong region stretched across each panel. - The capture blit source now uses the real framebuffer pixel size (logical size times ImGui's DisplayFramebufferScale, truncated the same way the GL backend does), while the capture and blur targets stay at logical size. The framebuffer size is tracked separately from the viewport size so quality or buffer-lifetime resets cannot lose it, and moving the window between a Retina and a non-Retina display redoes the capture. DX11 is unaffected; its scale is one. - The debug screenshot sweep captures at framebuffer size too, so HiDPI captures are no longer cropped. Tests: the framebuffer-extent helper matches the GL backend's truncation and falls back to the logical size when the scale is unknown. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
288 lines
8.1 KiB
C++
288 lines
8.1 KiB
C++
// DragonX Wallet - ImGui Edition
|
|
// Copyright 2024-2026 The Hush Developers
|
|
// Released under the GPLv3
|
|
|
|
#include "framebuffer.h"
|
|
|
|
#ifdef DRAGONX_HAS_GLAD
|
|
#include <glad/gl.h>
|
|
#include <cstdio>
|
|
#include "../../util/logger.h"
|
|
|
|
// GL 3.0 constants — may be missing from some GLAD configurations
|
|
#ifndef GL_RGBA16F
|
|
#define GL_RGBA16F 0x881A
|
|
#endif
|
|
#ifndef GL_HALF_FLOAT
|
|
#define GL_HALF_FLOAT 0x140B
|
|
#endif
|
|
|
|
namespace dragonx {
|
|
namespace ui {
|
|
namespace effects {
|
|
|
|
// ============================================================================
|
|
// Framebuffer Implementation (GLEW Available)
|
|
// ============================================================================
|
|
|
|
Framebuffer::Framebuffer() = default;
|
|
|
|
Framebuffer::~Framebuffer()
|
|
{
|
|
destroy();
|
|
}
|
|
|
|
Framebuffer::Framebuffer(Framebuffer&& other) noexcept
|
|
: fbo_(other.fbo_)
|
|
, colorTexture_(other.colorTexture_)
|
|
, width_(other.width_)
|
|
, height_(other.height_)
|
|
, isComplete_(other.isComplete_)
|
|
{
|
|
other.fbo_ = 0;
|
|
other.colorTexture_ = 0;
|
|
other.width_ = 0;
|
|
other.height_ = 0;
|
|
other.isComplete_ = false;
|
|
}
|
|
|
|
Framebuffer& Framebuffer::operator=(Framebuffer&& other) noexcept
|
|
{
|
|
if (this != &other) {
|
|
destroy();
|
|
|
|
fbo_ = other.fbo_;
|
|
colorTexture_ = other.colorTexture_;
|
|
width_ = other.width_;
|
|
height_ = other.height_;
|
|
isComplete_ = other.isComplete_;
|
|
|
|
other.fbo_ = 0;
|
|
other.colorTexture_ = 0;
|
|
other.width_ = 0;
|
|
other.height_ = 0;
|
|
other.isComplete_ = false;
|
|
}
|
|
return *this;
|
|
}
|
|
|
|
bool Framebuffer::init(int width, int height, bool useFloat16)
|
|
{
|
|
if (width <= 0 || height <= 0) {
|
|
DEBUG_LOGF("Framebuffer::init - Invalid dimensions: %dx%d\n", width, height);
|
|
return false;
|
|
}
|
|
|
|
// Clean up existing resources
|
|
cleanup();
|
|
|
|
width_ = width;
|
|
height_ = height;
|
|
useFloat16_ = useFloat16;
|
|
|
|
// Generate framebuffer
|
|
glGenFramebuffers(1, &fbo_);
|
|
glBindFramebuffer(GL_FRAMEBUFFER, fbo_);
|
|
|
|
// Create color texture — use RGBA16F for blur intermediates to avoid banding
|
|
glGenTextures(1, &colorTexture_);
|
|
glBindTexture(GL_TEXTURE_2D, colorTexture_);
|
|
if (useFloat16) {
|
|
glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA16F, width, height, 0, GL_RGBA, GL_HALF_FLOAT, nullptr);
|
|
} else {
|
|
glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA8, width, height, 0, GL_RGBA, GL_UNSIGNED_BYTE, nullptr);
|
|
}
|
|
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
|
|
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
|
|
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
|
|
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);
|
|
|
|
// Attach color texture to framebuffer
|
|
glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, colorTexture_, 0);
|
|
|
|
// Check completeness
|
|
isComplete_ = checkComplete();
|
|
|
|
// Unbind
|
|
glBindFramebuffer(GL_FRAMEBUFFER, 0);
|
|
glBindTexture(GL_TEXTURE_2D, 0);
|
|
|
|
if (!isComplete_) {
|
|
DEBUG_LOGF("Framebuffer::init - Framebuffer is not complete!\n");
|
|
cleanup();
|
|
return false;
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
bool Framebuffer::resize(int width, int height)
|
|
{
|
|
if (width == width_ && height == height_) {
|
|
return true; // No change needed
|
|
}
|
|
return init(width, height, useFloat16_);
|
|
}
|
|
|
|
void Framebuffer::destroy()
|
|
{
|
|
cleanup();
|
|
}
|
|
|
|
void Framebuffer::cleanup()
|
|
{
|
|
if (colorTexture_) {
|
|
glDeleteTextures(1, &colorTexture_);
|
|
colorTexture_ = 0;
|
|
}
|
|
if (fbo_) {
|
|
glDeleteFramebuffers(1, &fbo_);
|
|
fbo_ = 0;
|
|
}
|
|
width_ = 0;
|
|
height_ = 0;
|
|
isComplete_ = false;
|
|
}
|
|
|
|
bool Framebuffer::checkComplete()
|
|
{
|
|
GLenum status = glCheckFramebufferStatus(GL_FRAMEBUFFER);
|
|
if (status != GL_FRAMEBUFFER_COMPLETE) {
|
|
const char* errorStr = "Unknown";
|
|
switch (status) {
|
|
case GL_FRAMEBUFFER_UNDEFINED: errorStr = "UNDEFINED"; break;
|
|
case GL_FRAMEBUFFER_INCOMPLETE_ATTACHMENT: errorStr = "INCOMPLETE_ATTACHMENT"; break;
|
|
case GL_FRAMEBUFFER_INCOMPLETE_MISSING_ATTACHMENT: errorStr = "INCOMPLETE_MISSING_ATTACHMENT"; break;
|
|
case GL_FRAMEBUFFER_INCOMPLETE_DRAW_BUFFER: errorStr = "INCOMPLETE_DRAW_BUFFER"; break;
|
|
case GL_FRAMEBUFFER_INCOMPLETE_READ_BUFFER: errorStr = "INCOMPLETE_READ_BUFFER"; break;
|
|
case GL_FRAMEBUFFER_UNSUPPORTED: errorStr = "UNSUPPORTED"; break;
|
|
case GL_FRAMEBUFFER_INCOMPLETE_MULTISAMPLE: errorStr = "INCOMPLETE_MULTISAMPLE"; break;
|
|
case GL_FRAMEBUFFER_INCOMPLETE_LAYER_TARGETS: errorStr = "INCOMPLETE_LAYER_TARGETS"; break;
|
|
}
|
|
DEBUG_LOGF("Framebuffer error: %s (0x%X)\n", errorStr, status);
|
|
return false;
|
|
}
|
|
return true;
|
|
}
|
|
|
|
void Framebuffer::bind()
|
|
{
|
|
glBindFramebuffer(GL_FRAMEBUFFER, fbo_);
|
|
glViewport(0, 0, width_, height_);
|
|
}
|
|
|
|
void Framebuffer::unbind()
|
|
{
|
|
glBindFramebuffer(GL_FRAMEBUFFER, 0);
|
|
}
|
|
|
|
void Framebuffer::clear(float r, float g, float b, float a)
|
|
{
|
|
glClearColor(r, g, b, a);
|
|
glClear(GL_COLOR_BUFFER_BIT);
|
|
}
|
|
|
|
void Framebuffer::blitFrom(GLuint srcFbo, int srcX, int srcY, int srcWidth, int srcHeight,
|
|
int dstX, int dstY)
|
|
{
|
|
glBindFramebuffer(GL_READ_FRAMEBUFFER, srcFbo);
|
|
glBindFramebuffer(GL_DRAW_FRAMEBUFFER, fbo_);
|
|
|
|
glBlitFramebuffer(
|
|
srcX, srcY, srcX + srcWidth, srcY + srcHeight, // Source rect
|
|
dstX, dstY, dstX + srcWidth, dstY + srcHeight, // Dest rect
|
|
GL_COLOR_BUFFER_BIT,
|
|
GL_LINEAR
|
|
);
|
|
|
|
glBindFramebuffer(GL_FRAMEBUFFER, 0);
|
|
}
|
|
|
|
void Framebuffer::captureScreen(int srcWidth, int srcHeight)
|
|
{
|
|
// Blit from default framebuffer (0) to this framebuffer
|
|
glBindFramebuffer(GL_READ_FRAMEBUFFER, 0);
|
|
glBindFramebuffer(GL_DRAW_FRAMEBUFFER, fbo_);
|
|
|
|
glBlitFramebuffer(
|
|
0, 0, srcWidth, srcHeight, // Source (screen)
|
|
0, 0, width_, height_, // Dest (this FBO)
|
|
GL_COLOR_BUFFER_BIT,
|
|
GL_LINEAR
|
|
);
|
|
|
|
glBindFramebuffer(GL_FRAMEBUFFER, 0);
|
|
}
|
|
|
|
// ============================================================================
|
|
// FramebufferPingPong Implementation
|
|
// ============================================================================
|
|
|
|
bool FramebufferPingPong::init(int width, int height, bool useFloat16)
|
|
{
|
|
if (!buffers_[0].init(width, height, useFloat16)) return false;
|
|
if (!buffers_[1].init(width, height, useFloat16)) return false;
|
|
currentSource_ = 0;
|
|
return true;
|
|
}
|
|
|
|
bool FramebufferPingPong::resize(int width, int height)
|
|
{
|
|
if (!buffers_[0].resize(width, height)) return false;
|
|
if (!buffers_[1].resize(width, height)) return false;
|
|
return true;
|
|
}
|
|
|
|
void FramebufferPingPong::destroy()
|
|
{
|
|
buffers_[0].destroy();
|
|
buffers_[1].destroy();
|
|
currentSource_ = 0;
|
|
}
|
|
|
|
void FramebufferPingPong::swap()
|
|
{
|
|
currentSource_ = 1 - currentSource_;
|
|
}
|
|
|
|
} // namespace effects
|
|
} // namespace ui
|
|
} // namespace dragonx
|
|
|
|
#else // !DRAGONX_HAS_GLAD
|
|
|
|
// ============================================================================
|
|
// Stub Implementation (No GLAD - Acrylic effects disabled)
|
|
// ============================================================================
|
|
|
|
namespace dragonx {
|
|
namespace ui {
|
|
namespace effects {
|
|
|
|
Framebuffer::Framebuffer() = default;
|
|
Framebuffer::~Framebuffer() = default;
|
|
Framebuffer::Framebuffer(Framebuffer&&) noexcept = default;
|
|
Framebuffer& Framebuffer::operator=(Framebuffer&&) noexcept = default;
|
|
|
|
bool Framebuffer::init(int, int, bool) { return false; }
|
|
bool Framebuffer::resize(int, int) { return false; }
|
|
void Framebuffer::destroy() {}
|
|
void Framebuffer::cleanup() {}
|
|
bool Framebuffer::checkComplete() { return false; }
|
|
void Framebuffer::bind() {}
|
|
void Framebuffer::unbind() {}
|
|
void Framebuffer::clear(float, float, float, float) {}
|
|
void Framebuffer::blitFrom(unsigned int, int, int, int, int, int, int) {}
|
|
void Framebuffer::captureScreen(int, int) {}
|
|
|
|
bool FramebufferPingPong::init(int, int, bool) { return false; }
|
|
bool FramebufferPingPong::resize(int, int) { return false; }
|
|
void FramebufferPingPong::destroy() {}
|
|
void FramebufferPingPong::swap() {}
|
|
|
|
} // namespace effects
|
|
} // namespace ui
|
|
} // namespace dragonx
|
|
|
|
#endif // DRAGONX_HAS_GLAD
|