// 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 #include 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(wglGetProcAddress("wglSwapIntervalEXT"))) { swap_interval(0); } return true; } void render_and_present(std::uint32_t frame) override { const GLsizei w = static_cast(width_); const GLsizei h = static_cast(height_); glViewport(0, 0, w, h); const FramePattern p = frame_pattern(frame, width_); // Animated background (whole framebuffer). glDisable(GL_SCISSOR_TEST); glClearColor(p.bg_r / 255.0f, p.bg_g / 255.0f, p.bg_b / 255.0f, 1.0f); glClear(GL_COLOR_BUFFER_BIT); glEnable(GL_SCISSOR_TEST); // Moving vertical bar (full height). glScissor(static_cast(p.bar_x), 0, static_cast(kBarWidth), 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. glScissor(0, h - static_cast(kFrameBlock), static_cast(kFrameBlock), static_cast(kFrameBlock)); glClearColor(p.code_r / 255.0f, p.code_g / 255.0f, p.code_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 create_gl_backend() { return std::make_unique(); } } // namespace coop::mock