Files
ObsidianDragon/src/ui/effects/framebuffer.cpp
T
DanSandClaude Opus 4.8 968d6d3875 fix(effects): capture the acrylic backdrop at framebuffer size on HiDPI
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>
2026-10-13 13:30:20 -05:00

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