diff --git a/src/spice2x/CMakeLists.txt b/src/spice2x/CMakeLists.txt index dca99bd..fd7ab6d 100644 --- a/src/spice2x/CMakeLists.txt +++ b/src/spice2x/CMakeLists.txt @@ -647,6 +647,7 @@ set(SOURCE_FILES ${SOURCE_FILES} stubs/stubs.cpp # touch + touch/gdi_overlay.cpp touch/touch.cpp touch/touch_gestures.cpp touch/win7.cpp diff --git a/src/spice2x/touch/gdi_overlay.cpp b/src/spice2x/touch/gdi_overlay.cpp new file mode 100644 index 0000000..09910b2 --- /dev/null +++ b/src/spice2x/touch/gdi_overlay.cpp @@ -0,0 +1,213 @@ +#include "gdi_overlay.h" + +#include + +#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); +} diff --git a/src/spice2x/touch/gdi_overlay.h b/src/spice2x/touch/gdi_overlay.h new file mode 100644 index 0000000..6ce19c2 --- /dev/null +++ b/src/spice2x/touch/gdi_overlay.h @@ -0,0 +1,21 @@ +#pragma once + +#include +#include + +// prepares a complete offscreen frame and returns its drawing DC; returns null on failure +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); + +// presents the frame prepared by the most recent successful begin call +void touch_gdi_overlay_present(HDC target_dc); + +// releases all cached GDI resources +void touch_gdi_overlay_release(); diff --git a/src/spice2x/touch/touch.cpp b/src/spice2x/touch/touch.cpp index cee87ab..4fd9bdf 100644 --- a/src/spice2x/touch/touch.cpp +++ b/src/spice2x/touch/touch.cpp @@ -23,6 +23,7 @@ #include "util/time.h" #include "util/utils.h" +#include "gdi_overlay.h" #include "handler.h" #include "touch_gestures.h" #include "win7.h" @@ -38,6 +39,16 @@ static const int TOUCH_EVENT_BUFFER_SIZE = 1024 * 4; static const int TOUCH_EVENT_BUFFER_THRESHOLD1 = 1024 * 2; static const int TOUCH_EVENT_BUFFER_THRESHOLD2 = 1024 * 3; +// timer id for the overlay repaint tick +static const UINT_PTR SPICETOUCH_OVERLAY_TIMER_ID = 1; + +// overlay repaint interval; the WinXP-compat build stays at 30 FPS +#if !SPICE_XP +static const int SPICETOUCH_OVERLAY_TIMER_MS = 1000 / 60; +#else +static const int SPICETOUCH_OVERLAY_TIMER_MS = 1000 / 30; +#endif // !SPICE_XP + // in mainline spicetools, this was false (show by default) // in spice2x, this is true (hide by default) bool SPICETOUCH_CARD_DISABLE = true; @@ -309,7 +320,10 @@ static LRESULT CALLBACK SpiceTouchWndProc(HWND hWnd, UINT msg, WPARAM wParam, LP break; } case WM_TIMER: { - InvalidateRect(hWnd, NULL, TRUE); + + // request a repaint; the frame is composed into an offscreen buffer, so no + // background erase is needed (bErase = FALSE avoids a transparent flash) + InvalidateRect(hWnd, NULL, FALSE); break; } case WM_PAINT: { @@ -341,45 +355,47 @@ static LRESULT CALLBACK SpiceTouchWndProc(HWND hWnd, UINT msg, WPARAM wParam, LP SWP_NOZORDER | SWP_NOREDRAW | SWP_NOREPOSITION | SWP_NOACTIVATE); } - // render the software overlay to a bitmap BEFORE BeginPaint: the imgui - // render is expensive, and running it between the background erase and the - // blit leaves the window transparent long enough to flicker - HBITMAP overlay_bitmap = nullptr; + // render the software overlay before BeginPaint so only GDI composition + // and the final blit happen while the window is being painted int overlay_width = 0, overlay_height = 0; + uint32_t *overlay_pixels = nullptr; + bool overlay_pixels_dirty = false; if (overlay_enabled) { overlay::OVERLAY->update(); overlay::OVERLAY->new_frame(); overlay::OVERLAY->render(); - uint32_t *pixel_data = overlay::OVERLAY.get()->sw_get_pixel_data(&overlay_width, &overlay_height); - if (overlay_width > 0 && overlay_height > 0) { - overlay_bitmap = CreateBitmap(overlay_width, overlay_height, 1, 8 * sizeof(uint32_t), pixel_data); - } + overlay_pixels = overlay::OVERLAY.get()->sw_get_pixel_data( + &overlay_width, &overlay_height); + overlay_pixels_dirty = overlay::OVERLAY->sw_pixels_dirty; } bool overlay_active = overlay_enabled && overlay::OVERLAY->get_active(); - // draw everything in a single BeginPaint/EndPaint (a WM_PAINT has one update - // region, so a second BeginPaint would get an empty region), keeping only - // fast blits between the background erase and EndPaint + // compose the whole frame into an offscreen back buffer and present it with a + // single blit; the window never shows a half-erased (transparent) surface + // mid-paint, which is what caused the occasional flicker at higher frame rates PAINTSTRUCT paint {}; HDC hdc = BeginPaint(hWnd, &paint); - SetBkMode(hdc, TRANSPARENT); - // blit the overlay bitmap - if (overlay_bitmap) { + RECT bufferRect {}; + GetClientRect(hWnd, &bufferRect); + int buffer_width = bufferRect.right - bufferRect.left; + int buffer_height = bufferRect.bottom - bufferRect.top; - /* - * draw bitmap - * - this currently sets the background to black because of SRCCOPY - * - SRCPAINT will blend but colors are wrong - * - once this is figured out we could also try hooking WM_PAINT and - * draw directly to the game window - */ - HDC hdcMem = CreateCompatibleDC(hdc); - SelectObject(hdcMem, overlay_bitmap); - BitBlt(hdc, 0, 0, overlay_width, overlay_height, hdcMem, 0, 0, SRCCOPY); - DeleteDC(hdcMem); - DeleteObject(overlay_bitmap); + HBRUSH color_key_brush = + (HBRUSH) GetClassLongPtr(hWnd, GCLP_HBRBACKGROUND); + HDC draw_dc = touch_gdi_overlay_begin_frame( + hdc, + color_key_brush, + buffer_width, + buffer_height, + overlay_pixels, + overlay_pixels_dirty, + overlay_width, + overlay_height); + if (draw_dc == nullptr) { + EndPaint(hWnd, &paint); + return 0; } // draw the insert-card button below the jubeat debug overlay; it is @@ -414,7 +430,7 @@ static LRESULT CALLBACK SpiceTouchWndProc(HWND hWnd, UINT msg, WPARAM wParam, LP SPICETOUCH_CARD_RECT = boxRect; // draw borders - FillRect(hdc, &boxRect, brushBorder); + FillRect(draw_dc, &boxRect, brushBorder); // modify box rect boxRect.left += 1; @@ -423,7 +439,7 @@ static LRESULT CALLBACK SpiceTouchWndProc(HWND hWnd, UINT msg, WPARAM wParam, LP boxRect.bottom -= 1; // fill box - FillRect(hdc, &boxRect, brushFill); + FillRect(draw_dc, &boxRect, brushFill); // modify box rect if (should_rotate) { @@ -435,19 +451,24 @@ static LRESULT CALLBACK SpiceTouchWndProc(HWND hWnd, UINT msg, WPARAM wParam, LP } // draw text - SelectObject(hdc, SPICETOUCH_FONT); - SetTextColor(hdc, RGB(0, 196, 0)); - DrawText(hdc, INSERT_CARD_TEXT, -1, &boxRect, DT_LEFT | DT_BOTTOM | DT_NOCLIP); + SelectObject(draw_dc, SPICETOUCH_FONT); + SetTextColor(draw_dc, RGB(0, 196, 0)); + DrawText(draw_dc, INSERT_CARD_TEXT, -1, &boxRect, DT_LEFT | DT_BOTTOM | DT_NOCLIP); // delete objects DeleteObject(brushFill); DeleteObject(brushBorder); } +#if !SPICE_XP // draw the jubeat debug overlay on top (hidden while the overlay is active) if (overlay_enabled && !overlay_active && games::jb::touch_debug_overlay_enabled()) { - games::jb::touch_draw_debug_overlay(hdc); + games::jb::touch_draw_debug_overlay(draw_dc); } +#endif // !SPICE_XP + + // present the composed frame in a single blit + touch_gdi_overlay_present(hdc); EndPaint(hWnd, &paint); return 0; @@ -480,6 +501,11 @@ static LRESULT CALLBACK SpiceTouchWndProc(HWND hWnd, UINT msg, WPARAM wParam, LP return 0; } case WM_DESTROY: { + touch_gdi_overlay_release(); + if (SPICETOUCH_FONT != nullptr) { + DeleteObject(SPICETOUCH_FONT); + SPICETOUCH_FONT = nullptr; + } PostQuitMessage(0); return 0; } @@ -768,8 +794,8 @@ void touch_create_wnd(HWND hWnd, bool overlay) { // create instance overlay::OVERLAY.reset(new overlay::SpiceOverlay(touch_window)); - // draw overlay in 30 FPS - SetTimer(touch_window, 1, 1000 / 30, NULL); + // draw overlay repaint timer (30 FPS on WinXP, 60 FPS otherwise) + SetTimer(touch_window, SPICETOUCH_OVERLAY_TIMER_ID, SPICETOUCH_OVERLAY_TIMER_MS, NULL); } }