// 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; } // vsync if available (a game-like cadence; avoids spinning uncapped). Runtime extension // lookup -- no loader/submodule needed. using PFN_wglSwapIntervalEXT = BOOL(WINAPI*)(int); if (auto swap_interval = reinterpret_cast(wglGetProcAddress("wglSwapIntervalEXT"))) { swap_interval(1); } 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); // Animated background (whole framebuffer). glDisable(GL_SCISSOR_TEST); glClearColor(static_cast((frame * 2) % 256) / 255.0f, static_cast((frame * 3) % 256) / 255.0f, static_cast((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((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(kFrameBlock), static_cast(kFrameBlock), static_cast(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 create_gl_backend() { return std::make_unique(); } } // namespace coop::mock