The mock backends presented with vsync ("a game-like cadence") -- wrong for a
perf/stress fixture: it does trivial work on an RTX 4090, so it must run as fast
as it can. Vsync capped them to tens of fps (dx9 30, dx10 23, dx11 63, dx12 126),
which hid both capture-induced slowdowns and the hook-removal race. Uncapped now:
dx9/dx10 INTERVAL_IMMEDIATE / Present(0,0) (BLT), dx11/dx12 ALLOW_TEARING +
Present(0, ALLOW_TEARING) (flip), gl wglSwapIntervalEXT(0), vk IMMEDIATE/MAILBOX.
Measured no-hook: dx9 ~21000, dx10 ~2800, dx11 ~17000, dx12 ~12000, gl ~26000, vk
~24000 fps.
mock_game_test now adds a present-rate floor per backend (>= 300/s while
capturing): with the hook live every backend stays in the hundreds-thousands
(vk 13500, dx11 9000+, gl 1800, dx9/10 ~1000-1600, dx12 2500). This is the
dimension the frame-advance checks missed -- the Vulkan 144->3 FPS stall still
advanced frames -- so it catches a present-thread stall OR an accidental vsync.
The faster storm exposed the hook-removal UAF fixed in the previous commit.
README roadmap + lessons-learned updated (incl. correcting the old "reset makes
in-flight trampoline calls safe" claim). Full suite 21/21.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
126 lines
3.6 KiB
C++
126 lines
3.6 KiB
C++
// OpenGL backend for the mock game. Renders the animated pattern with scissored clears
|
|
// (glClear + glScissor -- all GL 1.1, exported straight from opengl32, so **no loader /
|
|
// submodule** is needed) and presents with SwapBuffers, so the OpenGL capture hook
|
|
// (SwapBuffers / wglSwapBuffers + glReadPixels) sees a genuine GL present.
|
|
//
|
|
// GL's framebuffer is bottom-left origin and the capture flips it vertically to top-down, so
|
|
// the frame-counter block is drawn at the GL *top* (y = h-block) to land at the captured
|
|
// image's top-left where the test samples it.
|
|
#include "render_backend.hpp"
|
|
|
|
#include <windows.h>
|
|
|
|
#include <GL/gl.h>
|
|
|
|
namespace coop::mock
|
|
{
|
|
namespace
|
|
{
|
|
class GlBackend : public RenderBackend
|
|
{
|
|
public:
|
|
bool init(HWND hwnd, std::uint32_t width, std::uint32_t height) override
|
|
{
|
|
width_ = width;
|
|
height_ = height;
|
|
hwnd_ = hwnd;
|
|
hdc_ = GetDC(hwnd);
|
|
if (hdc_ == nullptr)
|
|
{
|
|
return false;
|
|
}
|
|
PIXELFORMATDESCRIPTOR pfd = {};
|
|
pfd.nSize = sizeof(pfd);
|
|
pfd.nVersion = 1;
|
|
pfd.dwFlags = PFD_DRAW_TO_WINDOW | PFD_SUPPORT_OPENGL | PFD_DOUBLEBUFFER;
|
|
pfd.iPixelType = PFD_TYPE_RGBA;
|
|
pfd.cColorBits = 32;
|
|
pfd.cAlphaBits = 8;
|
|
pfd.iLayerType = PFD_MAIN_PLANE;
|
|
const int pf = ChoosePixelFormat(hdc_, &pfd);
|
|
if (pf == 0 || !SetPixelFormat(hdc_, pf, &pfd))
|
|
{
|
|
return false;
|
|
}
|
|
hglrc_ = wglCreateContext(hdc_); // legacy context is enough for GL 1.1 clears
|
|
if (hglrc_ == nullptr || !wglMakeCurrent(hdc_, hglrc_))
|
|
{
|
|
return false;
|
|
}
|
|
// Uncapped: the mock is a perf fixture and must run as fast as it can (disable vsync), so a
|
|
// capture-induced slowdown is visible. Runtime extension lookup -- no loader/submodule needed.
|
|
using PFN_wglSwapIntervalEXT = BOOL(WINAPI*)(int);
|
|
if (auto swap_interval = reinterpret_cast<PFN_wglSwapIntervalEXT>(wglGetProcAddress("wglSwapIntervalEXT")))
|
|
{
|
|
swap_interval(0);
|
|
}
|
|
return true;
|
|
}
|
|
|
|
void render_and_present(std::uint32_t frame) override
|
|
{
|
|
const GLsizei w = static_cast<GLsizei>(width_);
|
|
const GLsizei h = static_cast<GLsizei>(height_);
|
|
glViewport(0, 0, w, h);
|
|
|
|
// Animated background (whole framebuffer).
|
|
glDisable(GL_SCISSOR_TEST);
|
|
glClearColor(static_cast<float>((frame * 2) % 256) / 255.0f, static_cast<float>((frame * 3) % 256) / 255.0f,
|
|
static_cast<float>((frame * 5) % 256) / 255.0f, 1.0f);
|
|
glClear(GL_COLOR_BUFFER_BIT);
|
|
|
|
glEnable(GL_SCISSOR_TEST);
|
|
// Moving vertical bar (full height).
|
|
const std::uint32_t span = width_ > 24 ? width_ - 24 : 1;
|
|
const GLint bx = static_cast<GLint>((frame * 4) % span);
|
|
glScissor(bx, 0, 24, h);
|
|
glClearColor(1.0f, 1.0f, 1.0f, 1.0f);
|
|
glClear(GL_COLOR_BUFFER_BIT);
|
|
|
|
// Frame-counter block at the GL top-left (y = h - block) so it lands top-left after the
|
|
// capture's vertical flip.
|
|
std::uint8_t r = 0, g = 0, b = 0;
|
|
frame_to_rgb(frame, r, g, b);
|
|
glScissor(0, h - static_cast<GLint>(kFrameBlock), static_cast<GLsizei>(kFrameBlock),
|
|
static_cast<GLsizei>(kFrameBlock));
|
|
glClearColor(r / 255.0f, g / 255.0f, b / 255.0f, 1.0f);
|
|
glClear(GL_COLOR_BUFFER_BIT);
|
|
glDisable(GL_SCISSOR_TEST);
|
|
|
|
SwapBuffers(hdc_); // the capture hook intercepts this
|
|
}
|
|
|
|
[[nodiscard]] const char* name() const override
|
|
{
|
|
return "gl";
|
|
}
|
|
|
|
~GlBackend() override
|
|
{
|
|
wglMakeCurrent(nullptr, nullptr);
|
|
if (hglrc_ != nullptr)
|
|
{
|
|
wglDeleteContext(hglrc_);
|
|
}
|
|
if (hdc_ != nullptr && hwnd_ != nullptr)
|
|
{
|
|
ReleaseDC(hwnd_, hdc_);
|
|
}
|
|
}
|
|
|
|
private:
|
|
std::uint32_t width_ = 0;
|
|
std::uint32_t height_ = 0;
|
|
HWND hwnd_ = nullptr;
|
|
HDC hdc_ = nullptr;
|
|
HGLRC hglrc_ = nullptr;
|
|
};
|
|
} // namespace
|
|
|
|
std::unique_ptr<RenderBackend> create_gl_backend()
|
|
{
|
|
return std::make_unique<GlBackend>();
|
|
}
|
|
|
|
} // namespace coop::mock
|