jb, shogikai: double buffer overlay, run at ~60fps (#805)

## Link to GitHub Issue or related Pull Request, if one exists
n/a

## Description of change
GDI bit blit overlay used exclusively for jubeat & shogikai was running
at 30fps, which caused the Jubeat touch debug UI to not render fast
enough. Run it at 60 fps, and also perform double buffering to avoid
flickering.

This could have been scoped to JB but I made the change for both. XP
builds will keep running at 30fps since those people run on cabs with
ancient hardware.

## Testing
Tested jubeat and shogikai, overlay renders without flickering at 60 fps
This commit is contained in:
bicarus
2026-07-15 01:31:29 -07:00
committed by GitHub
parent 5c3617c05f
commit 1b6d1af951
4 changed files with 296 additions and 35 deletions
+213
View File
@@ -0,0 +1,213 @@
#include "gdi_overlay.h"
#include <cstddef>
#include "util/logging.h"
namespace {
// the back buffer matches the window DC; the software buffer has a fixed 32-bit layout
enum class BufferType {
TargetCompatible,
Bgra32,
};
struct GdiBuffer {
HDC dc = nullptr;
HBITMAP bitmap = nullptr;
// bitmap originally selected into the memory DC, restored before cleanup
HGDIOBJ old_bitmap = nullptr;
int width = 0;
int height = 0;
};
// back buffer holds the complete frame; overlay buffer holds ImGui software pixels
GdiBuffer BACK_BUFFER;
GdiBuffer OVERLAY_BUFFER;
void release_buffer(GdiBuffer &buffer) {
// destroy the DC before the bitmap so cleanup is safe even if restoration fails
if (buffer.dc != nullptr) {
if (buffer.old_bitmap != nullptr && buffer.old_bitmap != HGDI_ERROR) {
SelectObject(buffer.dc, buffer.old_bitmap);
}
DeleteDC(buffer.dc);
}
if (buffer.bitmap != nullptr) {
DeleteObject(buffer.bitmap);
}
buffer = {};
}
// ensures the buffer has a memory DC with a bitmap of the requested size and type
// selected into it. a matching allocation is reused; otherwise the old resources are
// released and recreated. returns false if the dimensions or any GDI operation fail.
bool ensure_buffer(
GdiBuffer &buffer,
HDC target_dc,
int width,
int height,
BufferType type,
const char *name) {
if (width <= 0 || height <= 0) {
return false;
}
if (buffer.dc != nullptr && buffer.bitmap != nullptr &&
buffer.width == width && buffer.height == height) {
return true;
}
// keep allocations across frames and recreate only after a size change
release_buffer(buffer);
buffer.dc = CreateCompatibleDC(target_dc);
if (buffer.dc == nullptr) {
log_warning("touch", "failed to create {} DC: {}", name, GetLastError());
return false;
}
// compatible bitmaps are fast presentation targets; BGRA bitmaps accept raw pixels
if (type == BufferType::TargetCompatible) {
buffer.bitmap = CreateCompatibleBitmap(target_dc, width, height);
} else {
buffer.bitmap = CreateBitmap(width, height, 1, sizeof(uint32_t) * 8, nullptr);
}
if (buffer.bitmap == nullptr) {
log_warning("touch", "failed to create {} bitmap: {}", name, GetLastError());
release_buffer(buffer);
return false;
}
buffer.old_bitmap = SelectObject(buffer.dc, buffer.bitmap);
if (buffer.old_bitmap == nullptr || buffer.old_bitmap == HGDI_ERROR) {
log_warning("touch", "failed to select {} bitmap: {}", name, GetLastError());
release_buffer(buffer);
return false;
}
buffer.width = width;
buffer.height = height;
return true;
}
bool update_overlay_buffer(
HDC target_dc,
const uint32_t *pixels,
bool pixels_dirty,
int width,
int height) {
if (pixels == nullptr) {
return false;
}
bool needs_update = pixels_dirty || OVERLAY_BUFFER.bitmap == nullptr ||
OVERLAY_BUFFER.width != width || OVERLAY_BUFFER.height != height;
if (!ensure_buffer(
OVERLAY_BUFFER,
target_dc,
width,
height,
BufferType::Bgra32,
"software overlay")) {
return false;
}
if (!needs_update) {
return true;
}
// SetDIBits requires the destination bitmap not to be selected into a DC
HGDIOBJ overlay_bitmap =
SelectObject(OVERLAY_BUFFER.dc, OVERLAY_BUFFER.old_bitmap);
if (overlay_bitmap == nullptr || overlay_bitmap == HGDI_ERROR) {
log_warning("touch", "failed to deselect software overlay bitmap: {}", GetLastError());
release_buffer(OVERLAY_BUFFER);
return false;
}
BITMAPINFO bitmap_info {};
bitmap_info.bmiHeader.biSize = sizeof(BITMAPINFOHEADER);
bitmap_info.bmiHeader.biWidth = width;
bitmap_info.bmiHeader.biHeight = -height;
bitmap_info.bmiHeader.biPlanes = 1;
bitmap_info.bmiHeader.biBitCount = sizeof(uint32_t) * 8;
bitmap_info.bmiHeader.biCompression = BI_RGB;
int copied_lines = SetDIBits(
target_dc,
OVERLAY_BUFFER.bitmap,
0,
height,
pixels,
&bitmap_info,
DIB_RGB_COLORS);
HGDIOBJ old_bitmap = SelectObject(OVERLAY_BUFFER.dc, OVERLAY_BUFFER.bitmap);
if (old_bitmap == nullptr || old_bitmap == HGDI_ERROR) {
log_warning("touch", "failed to reselect software overlay bitmap: {}", GetLastError());
release_buffer(OVERLAY_BUFFER);
return false;
}
OVERLAY_BUFFER.old_bitmap = old_bitmap;
if (copied_lines != height) {
log_warning("touch", "failed to update software overlay bitmap: {} of {} lines copied",
copied_lines, height);
release_buffer(OVERLAY_BUFFER);
return false;
}
return true;
}
}
HDC touch_gdi_overlay_begin_frame(
HDC target_dc,
HBRUSH background_brush,
int width,
int height,
const uint32_t *overlay_pixels,
bool overlay_pixels_dirty,
int overlay_width,
int overlay_height) {
if (!ensure_buffer(
BACK_BUFFER,
target_dc,
width,
height,
BufferType::TargetCompatible,
"overlay back buffer")) {
return nullptr;
}
HDC draw_dc = BACK_BUFFER.dc;
SetBkMode(draw_dc, TRANSPARENT);
// start each frame from the transparent color-key background
RECT buffer_rect {0, 0, width, height};
FillRect(draw_dc, &buffer_rect, background_brush);
if (update_overlay_buffer(
target_dc,
overlay_pixels,
overlay_pixels_dirty,
overlay_width,
overlay_height) &&
!BitBlt(draw_dc, 0, 0, overlay_width, overlay_height,
OVERLAY_BUFFER.dc, 0, 0, SRCCOPY)) {
log_warning("touch", "failed to draw software overlay bitmap: {}", GetLastError());
}
return draw_dc;
}
void touch_gdi_overlay_present(HDC target_dc) {
// one full-window blit exposes the completed frame without an intermediate erase
if (!BitBlt(target_dc, 0, 0, BACK_BUFFER.width, BACK_BUFFER.height,
BACK_BUFFER.dc, 0, 0, SRCCOPY)) {
log_warning("touch", "failed to present overlay back buffer: {}", GetLastError());
}
}
void touch_gdi_overlay_release() {
release_buffer(BACK_BUFFER);
release_buffer(OVERLAY_BUFFER);
}