// The OpenGL 4.6 backend. Same shaders and passes as Vulkan: // - textures are ARB_bindless_texture handles in a storage-buffer table, indexed exactly like // Vulkan's descriptor array; shadow maps also get comparison handles in a second table // - per-draw / per-frame data are storage buffers at fixed bindings (model.glsl); the push // constants become a small uniform block, rewritten per draw // - glClipControl(LOWER_LEFT, ZERO_TO_ONE): with GL's own origin, clip-space y = -1 lands in // memory row 0 exactly as in Vulkan (whose projection we share, y flipped), so images, // shadow-map tiles and gl_FragCoord all match; depth is 0..1 like Vulkan. The y flip reverses // window-space winding, so front faces are clockwise. Only the final blit to the window // (which shows row 0 at the bottom) flips. // Requires ARB_bindless_texture (NVIDIA, AMD); without it init fails and the engine uses Vulkan. #include "ui/ui.h" #include "render/backend.h" #include "render/screenshot.h" #include "rhi/gl/gl_loader.h" #include "core/log.h" #include "platform/window.h" #include #include #include #include #include #include #include #include namespace keel { namespace { // Every shader's source and the shared headers, embedded by the build (cmake/EmbedText.cmake). const std::map& shaderSources() { static const std::map sources = [] { struct Entry { const char* name; const char* text; }; static const Entry entries[] = { #include "gl_sources.inc" }; std::map m; for (const Entry& e : entries) m[e.name] = e.text; return m; }(); return sources; } // The source with #includes expanded and the GL preamble in place of its #version line. std::string expand(std::string_view name, int depth = 0) { const auto& sources = shaderSources(); const auto it = sources.find(name); if (it == sources.end() || depth > 8) { log::error("OpenGL: shader source {} not found", name); return {}; } std::string out; std::string_view text = it->second; while (!text.empty()) { const size_t eol = text.find('\n'); const std::string_view line = text.substr(0, eol); text = eol == std::string_view::npos ? std::string_view{} : text.substr(eol + 1); if (line.starts_with("#version")) { out += "#version 460 core\n#extension GL_ARB_bindless_texture : require\n#define KEEL_GL 1\n"; } else if (line.starts_with("#include \"")) { const size_t a = line.find('"') + 1, b = line.find('"', a); out += expand(line.substr(a, b - a), depth + 1); } else { out += line; out += '\n'; } } return out; } GLuint compile(GLenum type, std::string_view name) { const std::string src = expand(name); const char* text = src.c_str(); const GLuint s = gl::CreateShader(type); gl::ShaderSource(s, 1, &text, nullptr); gl::CompileShader(s); GLint ok = 0; gl::GetShaderiv(s, GL_COMPILE_STATUS, &ok); if (!ok) { char logText[4096]; gl::GetShaderInfoLog(s, sizeof logText, nullptr, logText); log::error("OpenGL: {} failed to compile:\n{}", name, logText); gl::DeleteShader(s); return 0; } return s; } GLuint program(std::string_view vs, std::string_view fs) { const GLuint v = compile(GL_VERTEX_SHADER, vs), f = fs.empty() ? 0 : compile(GL_FRAGMENT_SHADER, fs); if (!v || (!fs.empty() && !f)) return 0; const GLuint p = gl::CreateProgram(); gl::AttachShader(p, v); if (f) gl::AttachShader(p, f); gl::LinkProgram(p); gl::DeleteShader(v); if (f) gl::DeleteShader(f); GLint ok = 0; gl::GetProgramiv(p, GL_LINK_STATUS, &ok); if (!ok) { char logText[4096]; gl::GetProgramInfoLog(p, sizeof logText, nullptr, logText); log::error("OpenGL: {} + {} failed to link:\n{}", vs, fs, logText); gl::DeleteProgram(p); return 0; } return p; } void APIENTRY debugMessage(GLenum, GLenum type, GLuint, GLenum severity, GLsizei, const GLchar* message, const void*) { if (severity == GL_DEBUG_SEVERITY_NOTIFICATION) return; if (type == GL_DEBUG_TYPE_ERROR || severity == GL_DEBUG_SEVERITY_HIGH) log::error("[gl] {}", message); else log::warn("[gl] {}", message); } // Uniform blocks (std140) standing in for the push constants. struct ModelPush { glm::mat4 viewProj; uint32_t textures, tint, material, drawIndex, firstJoint, pad[3]; }; struct BoxPush { glm::mat4 viewProj; gpu::Box box; }; struct HzbPush { uint32_t depth, srcW, srcH, dstW, dstH, pad[3]; }; struct TonemapPush { uint32_t scene; float exposure; uint32_t ao, bloom; float bloomStrength, saturation, vignette, sharpen; uint32_t depth, pad[3]; glm::vec4 shafts, sunColor; }; struct SsaoPush { uint32_t depth, w, h, outW, outH; float radius, strength, pad; glm::vec4 proj; }; struct SsaoBlurPush { uint32_t ao, depth, w, h, outW, outH; float pad0, pad1; glm::vec4 proj; }; struct TaaPush { glm::mat4 reproject; uint32_t scene, history, depth, w, h, reset, ssr; float pad1; }; struct AdaptPush { uint32_t src, prev; float dt, speed, key, minE, maxE, pad; }; struct SpritePush { glm::mat4 viewProj; gpu::Sprite sprite; }; struct VolumetricPush { uint32_t depth, w, h, outW, outH; float pad[3]; glm::vec4 proj; }; struct SsrPush { uint32_t depth, gbuf, scene, w, h, outW, outH; float strength; glm::vec4 proj; }; struct BloomPush { uint32_t src, srcW, srcH, outW, outH; uint32_t extra; // down: first level; up: radius (float bits) uint32_t pad[2]; }; // Storage buffer bindings (model.glsl, backend_gl.glsl). enum Binding : GLuint { B_VERTICES = 0, B_SKIN = 1, B_JOINTS = 2, B_DRAWS = 3, B_FRAME = 4, B_LIGHTS = 5, B_CELLS = 6, B_INDICES = 7, B_VIEWS = 8, B_TEXTURES = 9, B_SHADOW_TEXTURES = 10, }; class OpenGLBackend final : public RenderBackend { public: const char* name() const override { return "OpenGL 4.6"; } GpuInfo gpuInfo() const override { return gpu_; } GpuInfo gpu_; bool init(const Window& window, bool validation) override { window_ = window.handle(); SDL_GL_SetAttribute(SDL_GL_CONTEXT_MAJOR_VERSION, 4); SDL_GL_SetAttribute(SDL_GL_CONTEXT_MINOR_VERSION, 6); SDL_GL_SetAttribute(SDL_GL_CONTEXT_PROFILE_MASK, SDL_GL_CONTEXT_PROFILE_CORE); SDL_GL_SetAttribute(SDL_GL_CONTEXT_FLAGS, validation ? SDL_GL_CONTEXT_DEBUG_FLAG : 0); SDL_GL_SetAttribute(SDL_GL_DOUBLEBUFFER, 1); SDL_GL_SetAttribute(SDL_GL_DEPTH_SIZE, 0); // we render into our own targets context_ = SDL_GL_CreateContext(window_); if (!context_) { log::warn("OpenGL 4.6 context: {}", SDL_GetError()); return false; } SDL_GL_MakeCurrent(window_, context_); if (!gl::load()) return false; bool bindless = false; GLint extensions = 0; gl::GetIntegerv(GL_NUM_EXTENSIONS, &extensions); for (GLint i = 0; i < extensions; ++i) bindless |= std::string_view(reinterpret_cast(gl::GetStringi(GL_EXTENSIONS, GLuint(i)))) == "GL_ARB_bindless_texture"; log::info("GPU: {} ({})", reinterpret_cast(gl::GetString(GL_RENDERER)), reinterpret_cast(gl::GetString(GL_VERSION))); gpu_.name = reinterpret_cast(gl::GetString(GL_RENDERER)), gpu_.driver = reinterpret_cast(gl::GetString(GL_VERSION)); if (!bindless) { log::warn("OpenGL: GL_ARB_bindless_texture isn't supported by this driver"); return false; } if (validation) { gl::Enable(GL_DEBUG_OUTPUT); gl::Enable(GL_DEBUG_OUTPUT_SYNCHRONOUS); gl::DebugMessageCallback(debugMessage, nullptr); } gl::ClipControl(GL_LOWER_LEFT, GL_ZERO_TO_ONE); gl::FrontFace(GL_CW); gl::PixelStorei(GL_UNPACK_ALIGNMENT, 1); gl::PixelStorei(GL_PACK_ALIGNMENT, 1); gl::CreateSamplers(1, &repeatSampler_); gl::SamplerParameteri(repeatSampler_, GL_TEXTURE_MIN_FILTER, GL_LINEAR_MIPMAP_LINEAR); gl::SamplerParameteri(repeatSampler_, GL_TEXTURE_MAG_FILTER, GL_LINEAR); gl::SamplerParameteri(repeatSampler_, GL_TEXTURE_WRAP_S, GL_REPEAT); gl::SamplerParameteri(repeatSampler_, GL_TEXTURE_WRAP_T, GL_REPEAT); gl::SamplerParameterf(repeatSampler_, GL_TEXTURE_MAX_ANISOTROPY, 16.0f); gl::CreateSamplers(1, &clampSampler_); gl::SamplerParameteri(clampSampler_, GL_TEXTURE_MIN_FILTER, GL_LINEAR); gl::SamplerParameteri(clampSampler_, GL_TEXTURE_MAG_FILTER, GL_LINEAR); gl::SamplerParameteri(clampSampler_, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE); gl::SamplerParameteri(clampSampler_, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE); gl::CreateSamplers(1, &uiRawSampler_); // (UI pictures: sRGB read as plain numbers, as ImGui's colours are) gl::SamplerParameteri(uiRawSampler_, GL_TEXTURE_MIN_FILTER, GL_LINEAR_MIPMAP_LINEAR); gl::SamplerParameteri(uiRawSampler_, GL_TEXTURE_MAG_FILTER, GL_LINEAR); gl::SamplerParameteri(uiRawSampler_, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE); gl::SamplerParameteri(uiRawSampler_, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE); gl::SamplerParameteri(uiRawSampler_, 0x8A48 /* GL_TEXTURE_SRGB_DECODE_EXT */, 0x8A4A /* GL_SKIP_DECODE_EXT */); gl::CreateSamplers(1, &compareSampler_); gl::SamplerParameteri(compareSampler_, GL_TEXTURE_MIN_FILTER, GL_LINEAR); gl::SamplerParameteri(compareSampler_, GL_TEXTURE_MAG_FILTER, GL_LINEAR); gl::SamplerParameteri(compareSampler_, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE); gl::SamplerParameteri(compareSampler_, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE); gl::SamplerParameteri(compareSampler_, GL_TEXTURE_COMPARE_MODE, GL_COMPARE_REF_TO_TEXTURE); gl::SamplerParameteri(compareSampler_, GL_TEXTURE_COMPARE_FUNC, GL_LEQUAL); auto buffer = [](GLsizeiptr size) { GLuint b; gl::CreateBuffers(1, &b); gl::NamedBufferStorage(b, size, nullptr, GL_DYNAMIC_STORAGE_BIT); return b; }; textureTable_ = buffer(kMaxTextures * sizeof(GLuint64)); shadowTable_ = buffer(kMaxTextures * sizeof(GLuint64)); draws_ = buffer(kMaxDraws * sizeof(gpu::DrawData)); joints_ = buffer(kMaxJointsPerFrame * sizeof(glm::mat4)); frame_ = buffer(sizeof(gpu::FrameData)); mirrorFrame_ = buffer(sizeof(gpu::FrameData)); lights_ = buffer(kMaxLights * sizeof(gpu::Light)); views_ = buffer(kMaxShadowViews * sizeof(gpu::ShadowView)); cells_ = buffer(kClusterX * kClusterY * kClusterZ * sizeof(glm::uvec2)); indices_ = buffer(kMaxLightIndices * sizeof(uint32_t)); push_ = buffer(256); empty_ = buffer(16); // bound where a shader declares a buffer the draw doesn't use gl::CreateVertexArrays(1, &vao_); meshProgram_ = program("mesh.vert", "mesh.frag"); skinnedProgram_ = program("skinned.vert", "mesh.frag"); shadowMeshProgram_ = program("mesh.vert", ""); shadowSkinnedProgram_ = program("skinned.vert", ""); boxProgram_ = program("box.vert", "box.frag"); tonemapProgram_ = program("fullscreen.vert", "tonemap.frag"); skyProgram_ = program("sky.vert", "sky.frag"); hzbProgram_ = program("fullscreen.vert", "hzb.frag"); ssaoProgram_ = program("fullscreen.vert", "ssao.frag"); ssaoBlurProgram_ = program("fullscreen.vert", "ssao_blur.frag"); bloomDownProgram_ = program("fullscreen.vert", "bloom_down.frag"); bloomUpProgram_ = program("fullscreen.vert", "bloom_up.frag"); taaProgram_ = program("fullscreen.vert", "taa.frag"); ssrProgram_ = program("fullscreen.vert", "ssr.frag"); adaptProgram_ = program("fullscreen.vert", "adapt.frag"); volumetricProgram_ = program("fullscreen.vert", "volumetric.frag"); spriteProgram_ = program("sprite.vert", "sprite.frag"); gl::CreateTextures(GL_TEXTURE_2D, 1, &hzbTex_); gl::TextureStorage2D(hzbTex_, 1, GL_R32F, GLsizei(kHzbWidth), GLsizei(kHzbHeight)); gl::CreateFramebuffers(1, &hzbFbo_); gl::NamedFramebufferTexture(hzbFbo_, GL_COLOR_ATTACHMENT0, hzbTex_, 0); gl::CreateBuffers(kHzbRing, hzbPbo_); for (GLuint b : hzbPbo_) gl::NamedBufferStorage(b, kHzbWidth * kHzbHeight * 4, nullptr, GL_MAP_READ_BIT | GL_CLIENT_STORAGE_BIT); if (!meshProgram_ || !skinnedProgram_ || !shadowMeshProgram_ || !shadowSkinnedProgram_ || !boxProgram_ || !tonemapProgram_ || !skyProgram_) return false; sunShadow_ = depthTarget(kShadowSize, sunShadowFbo_); atlas_ = depthTarget(kAtlasSize, atlasFbo_); setHandles(kSunShadowSlot, sunShadow_, clampSampler_, true); setHandles(kAtlasSlot, atlas_, clampSampler_, true); IMGUI_CHECKVERSION(); ImGui::CreateContext(); ImGui::GetIO().IniFilename = nullptr; ImGui::StyleColorsDark(); imgui_ = ImGui_ImplSDL3_InitForOpenGL(window_, context_) && ImGui_ImplOpenGL3_Init("#version 460"); return imgui_; } void shutdown() override { if (!context_) return; if (imgui_) { ImGui_ImplOpenGL3_Shutdown(); ImGui_ImplSDL3_Shutdown(); ImGui::DestroyContext(); imgui_ = false; } if (gl::DeleteBuffers) { for (auto& [slot, t] : textures_) releaseTexture(t); for (Model& m : models_) releaseModel(m); releaseTargets(); for (GLuint* t : {&sunShadow_.texture, &atlas_.texture}) gl::DeleteTextures(1, t); for (GLuint* f : {&sunShadowFbo_, &atlasFbo_, &hzbFbo_}) gl::DeleteFramebuffers(1, f); gl::DeleteTextures(1, &hzbTex_); for (int i = 0; i < kHzbRing; ++i) { if (hzbFence_[i]) gl::DeleteSync(hzbFence_[i]); gl::DeleteBuffers(1, &hzbPbo_[i]); } for (GLuint b : {textureTable_, shadowTable_, draws_, joints_, frame_, mirrorFrame_, lights_, views_, cells_, indices_, push_, empty_}) gl::DeleteBuffers(1, &b); for (GLuint s : {repeatSampler_, clampSampler_, compareSampler_, uiRawSampler_}) gl::DeleteSamplers(1, &s); for (GLuint p : {meshProgram_, skinnedProgram_, shadowMeshProgram_, shadowSkinnedProgram_, boxProgram_, tonemapProgram_, skyProgram_, hzbProgram_, ssaoProgram_, ssaoBlurProgram_, ssrProgram_, adaptProgram_, volumetricProgram_, spriteProgram_, bloomDownProgram_, bloomUpProgram_, taaProgram_}) gl::DeleteProgram(p); gl::DeleteVertexArrays(1, &vao_); } SDL_GL_DestroyContext(context_); context_ = nullptr; } void beginUi() override { ImGui_ImplOpenGL3_NewFrame(); ImGui_ImplSDL3_NewFrame(); ui::applyPixelScale(); ImGui::NewFrame(); } void setVsync(bool on) override { SDL_GL_SetSwapInterval(on ? 1 : 0); } void onResize() override {} bool blockCompression() const override { return true; } // BPTC (BC7) is core since 4.2 bool renderScale() const override { return true; } void createTexture(TextureId slot, uint32_t w, uint32_t h, bool srgb, const std::vector>& mips, TexFormat format = TexFormat::RGBA8) override { Texture t; gl::CreateTextures(GL_TEXTURE_2D, 1, &t.texture); if (format == TexFormat::BC7) { const GLenum f = srgb ? 0x8E8D /* GL_COMPRESSED_SRGB_ALPHA_BPTC_UNORM */ : 0x8E8C /* GL_COMPRESSED_RGBA_BPTC_UNORM */; gl::TextureStorage2D(t.texture, GLsizei(mips.size()), f, GLsizei(w), GLsizei(h)); for (size_t level = 0; level < mips.size(); ++level) gl::CompressedTextureSubImage2D(t.texture, GLint(level), 0, 0, GLsizei(std::max(1u, w >> level)), GLsizei(std::max(1u, h >> level)), f, GLsizei(mips[level].size()), mips[level].data()); } else { gl::TextureStorage2D(t.texture, GLsizei(mips.size()), srgb ? GL_SRGB8_ALPHA8 : GL_RGBA8, GLsizei(w), GLsizei(h)); for (size_t level = 0; level < mips.size(); ++level) gl::TextureSubImage2D(t.texture, GLint(level), 0, 0, GLsizei(std::max(1u, w >> level)), GLsizei(std::max(1u, h >> level)), GL_RGBA, GL_UNSIGNED_BYTE, mips[level].data()); } t.srgb = srgb; if (auto it = textures_.find(slot); it != textures_.end()) retiredTextures_.emplace_back(frameNumber_, it->second); // queued draws may use it setHandles(slot, t, repeatSampler_, false); // like Vulkan: one repeating sampler for every texture textures_[slot] = t; } void uploadModel(uint32_t index, const KmdlModel& kmdl) override { if (models_.size() <= index) models_.resize(index + 1); releaseModel(models_[index]); // GL keeps it alive while queued draws still use it Model m; auto upload = [](const void* data, size_t size) { GLuint b; gl::CreateBuffers(1, &b); gl::NamedBufferStorage(b, GLsizeiptr(size), data, 0); return b; }; if (!kmdl.vertices.empty() && !kmdl.indices.empty()) { m.vertices = upload(kmdl.vertices.data(), kmdl.vertices.size() * sizeof(KmdlVertex)); m.indices = upload(kmdl.indices.data(), kmdl.indices.size() * sizeof(uint32_t)); if (!kmdl.skin.empty()) m.skin = upload(kmdl.skin.data(), kmdl.skin.size() * sizeof(KmdlSkin)); } models_[index] = m; } uint64_t uiTexture(TextureId slot) override { const auto it = textures_.find(slot); return it == textures_.end() ? 0 : uint64_t(it->second.texture); } UiRawRead uiRawRead(TextureId slot) override { const auto it = textures_.find(slot); if (it == textures_.end() || !it->second.srgb) return {}; // (A sampler, not the texture's own setting: that's fixed once it has a bindless handle.) return {[](const ImDrawList*, const ImDrawCmd* c) { gl::BindSampler(0, GLuint(uintptr_t(c->UserCallbackData))); }, [](const ImDrawList*, const ImDrawCmd*) { gl::BindSampler(0, 0); }, reinterpret_cast(uintptr_t(uiRawSampler_))}; } bool beginFrame(uint32_t& width, uint32_t& height) override { int w = 0, h = 0; if (SDL_GetWindowFlags(window_) & SDL_WINDOW_MINIMIZED) return false; SDL_GetWindowSizeInPixels(window_, &w, &h); if (w <= 0 || h <= 0) return false; width = uint32_t(w); // (targets are sized to the scene in render) height = uint32_t(h); return true; } void render(FramePacket& p) override; // Reads ring slot i's grid if the GPU is done with it (or, wait: before reusing the slot). void collectHzb(int i, bool wait) { if (!hzbFence_[i]) return; const GLenum r = gl::ClientWaitSync(hzbFence_[i], wait ? GL_SYNC_FLUSH_COMMANDS_BIT : 0, wait ? 1'000'000'000 : 0); if (r != GL_ALREADY_SIGNALED && r != GL_CONDITION_SATISFIED) return; gl::DeleteSync(hzbFence_[i]); hzbFence_[i] = nullptr; if (hzbTags_[i] <= hzbLatest_.tag) return; if (const float* d = static_cast(gl::MapNamedBufferRange(hzbPbo_[i], 0, kHzbWidth * kHzbHeight * 4, GL_MAP_READ_BIT))) { hzbLatest_ = {hzbTags_[i], std::vector(d, d + kHzbWidth * kHzbHeight)}; gl::UnmapNamedBuffer(hzbPbo_[i]); } } bool takeHzb(HzbReadback& out) override { for (int i = 0; i < kHzbRing; ++i) collectHzb(i, false); if (hzbLatest_.tag <= hzbTaken_) return false; out = hzbLatest_; hzbTaken_ = hzbLatest_.tag; return true; } private: struct Texture { GLuint texture = 0; GLuint64 handle = 0, shadowHandle = 0; bool srgb = false; }; struct Model { GLuint vertices = 0, indices = 0, skin = 0; }; void setHandles(TextureId slot, Texture& t, GLuint sampler, bool depth) { t.handle = gl::GetTextureSamplerHandleARB(t.texture, sampler); gl::MakeTextureHandleResidentARB(t.handle); gl::NamedBufferSubData(textureTable_, GLintptr(slot) * 8, 8, &t.handle); if (depth) { t.shadowHandle = gl::GetTextureSamplerHandleARB(t.texture, compareSampler_); gl::MakeTextureHandleResidentARB(t.shadowHandle); gl::NamedBufferSubData(shadowTable_, GLintptr(slot) * 8, 8, &t.shadowHandle); } } void releaseTexture(Texture& t) { if (t.handle) gl::MakeTextureHandleNonResidentARB(t.handle); if (t.shadowHandle) gl::MakeTextureHandleNonResidentARB(t.shadowHandle); if (t.texture) gl::DeleteTextures(1, &t.texture); t = {}; } void releaseModel(Model& m) { for (GLuint* b : {&m.vertices, &m.indices, &m.skin}) if (*b) gl::DeleteBuffers(1, b); m = {}; } Texture depthTarget(uint32_t size, GLuint& fbo) { Texture t; gl::CreateTextures(GL_TEXTURE_2D, 1, &t.texture); gl::TextureStorage2D(t.texture, 1, GL_DEPTH_COMPONENT32F, GLsizei(size), GLsizei(size)); gl::CreateFramebuffers(1, &fbo); gl::NamedFramebufferTexture(fbo, GL_DEPTH_ATTACHMENT, t.texture, 0); gl::NamedFramebufferDrawBuffer(fbo, GL_NONE); return t; } void releaseTargets() { releaseTexture(scene_); releaseTexture(gbuf_); releaseTexture(mirror_); releaseTexture(mirrorGbuf_); if (mirrorDepth_) gl::DeleteTextures(1, &mirrorDepth_); if (mirrorFbo_) gl::DeleteFramebuffers(1, &mirrorFbo_); mirrorDepth_ = mirrorFbo_ = 0; releasePostTarget(ssr_); for (PostTarget& t : adapt_) releasePostTarget(t); releasePostTarget(ao_); releasePostTarget(aoBlur_); for (PostTarget& t : bloom_) releasePostTarget(t); for (PostTarget& t : taa_) releasePostTarget(t); if (depthTex_.handle) gl::MakeTextureHandleNonResidentARB(depthTex_.handle); depthTex_ = {}; if (sceneDepth_) gl::DeleteTextures(1, &sceneDepth_); if (sceneFbo_) gl::DeleteFramebuffers(1, &sceneFbo_); sceneDepth_ = sceneFbo_ = 0; } void ensureTargets(uint32_t w, uint32_t h) { if (scene_.texture && w == targetW_ && h == targetH_) return; releaseTargets(); gl::CreateTextures(GL_TEXTURE_2D, 1, &scene_.texture); gl::TextureStorage2D(scene_.texture, 1, GL_RGBA16F, GLsizei(w), GLsizei(h)); gl::CreateTextures(GL_TEXTURE_2D, 1, &sceneDepth_); gl::TextureStorage2D(sceneDepth_, 1, GL_DEPTH_COMPONENT32F, GLsizei(w), GLsizei(h)); gl::CreateFramebuffers(1, &sceneFbo_); gl::NamedFramebufferTexture(sceneFbo_, GL_COLOR_ATTACHMENT0, scene_.texture, 0); gl::NamedFramebufferTexture(sceneFbo_, GL_DEPTH_ATTACHMENT, sceneDepth_, 0); gl::CreateTextures(GL_TEXTURE_2D, 1, &gbuf_.texture); // the G-buffer: view normal, roughness, reflection weight gl::TextureStorage2D(gbuf_.texture, 1, GL_RGBA16F, GLsizei(w), GLsizei(h)); gl::NamedFramebufferTexture(sceneFbo_, GL_COLOR_ATTACHMENT1, gbuf_.texture, 0); const GLenum bufs[2] = {GL_COLOR_ATTACHMENT0, GL_COLOR_ATTACHMENT1}; gl::NamedFramebufferDrawBuffers(sceneFbo_, 2, bufs); setHandles(kGbufSlot, gbuf_, clampSampler_, false); if (gl::CheckNamedFramebufferStatus(sceneFbo_, GL_FRAMEBUFFER) != GL_FRAMEBUFFER_COMPLETE) log::error("OpenGL: scene framebuffer incomplete"); setHandles(kSceneSlot, scene_, clampSampler_, false); gl::CreateTextures(GL_TEXTURE_2D, 1, &mirror_.texture); gl::TextureStorage2D(mirror_.texture, 1, GL_RGBA16F, GLsizei(w), GLsizei(h)); gl::CreateTextures(GL_TEXTURE_2D, 1, &mirrorGbuf_.texture); gl::TextureStorage2D(mirrorGbuf_.texture, 1, GL_RGBA16F, GLsizei(w), GLsizei(h)); gl::CreateTextures(GL_TEXTURE_2D, 1, &mirrorDepth_); gl::TextureStorage2D(mirrorDepth_, 1, GL_DEPTH_COMPONENT32F, GLsizei(w), GLsizei(h)); gl::CreateFramebuffers(1, &mirrorFbo_); gl::NamedFramebufferTexture(mirrorFbo_, GL_COLOR_ATTACHMENT0, mirror_.texture, 0); gl::NamedFramebufferTexture(mirrorFbo_, GL_COLOR_ATTACHMENT1, mirrorGbuf_.texture, 0); gl::NamedFramebufferTexture(mirrorFbo_, GL_DEPTH_ATTACHMENT, mirrorDepth_, 0); gl::NamedFramebufferDrawBuffers(mirrorFbo_, 2, bufs); setHandles(kMirrorSlot, mirror_, clampSampler_, false); depthTex_.texture = sceneDepth_; setHandles(kDepthSlot, depthTex_, clampSampler_, false); ao_ = makePostTarget(kAoSlot, GL_R8, std::max(1u, w / 2), std::max(1u, h / 2)); aoBlur_ = makePostTarget(kAoBlurSlot, GL_R8, std::max(1u, w / 2), std::max(1u, h / 2)); ssr_ = makePostTarget(kSsrSlot, GL_RGBA16F, std::max(1u, w / 2), std::max(1u, h / 2)); for (uint32_t i = 0; i < 2; ++i) adapt_[i] = makePostTarget(kAdaptSlot + i, GL_RGBA16F, 1, 1); adaptValid_ = false; for (uint32_t i = 0; i < 2; ++i) taa_[i] = makePostTarget(kTaaSlot + i, GL_RGBA16F, w, h); for (uint32_t i = 0; i < kBloomLevels; ++i) bloom_[i] = makePostTarget(kBloomSlot + i, GL_RGBA16F, std::max(1u, w >> (i + 1)), std::max(1u, h >> (i + 1))); targetW_ = w; targetH_ = h; } template void push(const T& data) { gl::NamedBufferSubData(push_, 0, sizeof data, &data); } void drawMeshes(const std::vector& draws, const glm::mat4& viewProj, bool shadow, GLuint mesh, GLuint skinned); void shadowPasses(GLuint fbo, uint32_t size, const std::vector& passes); SDL_Window* window_ = nullptr; SDL_GLContext context_ = nullptr; bool imgui_ = false; GLuint repeatSampler_ = 0, clampSampler_ = 0, compareSampler_ = 0, uiRawSampler_ = 0; GLuint mirrorFrame_ = 0; // the mirror's pass's frame data GLuint textureTable_ = 0, shadowTable_ = 0, draws_ = 0, joints_ = 0, frame_ = 0, lights_ = 0, views_ = 0, cells_ = 0, indices_ = 0; GLuint push_ = 0, empty_ = 0, vao_ = 0; GLuint meshProgram_ = 0, skinnedProgram_ = 0, shadowMeshProgram_ = 0, shadowSkinnedProgram_ = 0, boxProgram_ = 0, tonemapProgram_ = 0, skyProgram_ = 0, hzbProgram_ = 0; GLuint ssaoProgram_ = 0, ssaoBlurProgram_ = 0, bloomDownProgram_ = 0, bloomUpProgram_ = 0, taaProgram_ = 0, ssrProgram_ = 0, adaptProgram_ = 0, volumetricProgram_ = 0, spriteProgram_ = 0; uint32_t adaptCur_ = 0; bool adaptValid_ = false; uint32_t taaCur_ = 0; uint64_t frameNumber_ = 0; std::vector> retiredTextures_; uint32_t shadowMapSize_ = kShadowSize; // Post-processing targets: a texture in the table and a framebuffer to draw into it. struct PostTarget { Texture tex; GLuint fbo = 0; uint32_t w = 0, h = 0; }; PostTarget ao_, aoBlur_, bloom_[kBloomLevels], taa_[2], ssr_, adapt_[2]; PostTarget makePostTarget(TextureId slot, GLenum format, uint32_t w, uint32_t h) { PostTarget t; t.w = w, t.h = h; gl::CreateTextures(GL_TEXTURE_2D, 1, &t.tex.texture); gl::TextureStorage2D(t.tex.texture, 1, format, GLsizei(w), GLsizei(h)); gl::CreateFramebuffers(1, &t.fbo); gl::NamedFramebufferTexture(t.fbo, GL_COLOR_ATTACHMENT0, t.tex.texture, 0); setHandles(slot, t.tex, clampSampler_, false); return t; } void releasePostTarget(PostTarget& t) { releaseTexture(t.tex); if (t.fbo) gl::DeleteFramebuffers(1, &t.fbo); t = {}; } // Occlusion grid: drawn, read into a pixel-pack buffer, picked up once its fence passes. static constexpr int kHzbRing = 3; GLuint hzbTex_ = 0, hzbFbo_ = 0, hzbPbo_[kHzbRing] = {}; GLsync hzbFence_[kHzbRing] = {}; uint64_t hzbTags_[kHzbRing] = {}; int hzbNext_ = 0; uint64_t hzbTaken_ = 0; HzbReadback hzbLatest_; Texture depthTex_; // the scene depth's handle (texture owned by sceneDepth_) Texture sunShadow_, atlas_, scene_, gbuf_; // Mirrors: the reflected scene (and its G-buffer and depth), the scene's size. Texture mirror_, mirrorGbuf_; GLuint mirrorDepth_ = 0, mirrorFbo_ = 0; bool mirrors() const override { return true; } GLuint sunShadowFbo_ = 0, atlasFbo_ = 0, sceneFbo_ = 0, sceneDepth_ = 0; uint32_t targetW_ = 0, targetH_ = 0; std::map textures_; std::vector models_; }; void OpenGLBackend::drawMeshes(const std::vector& draws, const glm::mat4& viewProj, bool shadow, GLuint mesh, GLuint skinned) { GLuint bound = 0; uint32_t boundModel = UINT32_MAX; ModelPush mp{}; mp.viewProj = viewProj; for (const MeshDraw& d : draws) { const Model& m = models_[d.model]; if (!m.indices) continue; const bool isSkinned = d.firstJoint != UINT32_MAX; const GLuint want = isSkinned ? skinned : mesh; if (want != bound) { gl::UseProgram(want); bound = want; } if (d.model != boundModel) { gl::VertexArrayElementBuffer(vao_, m.indices); gl::BindBufferBase(GL_SHADER_STORAGE_BUFFER, B_VERTICES, m.vertices); gl::BindBufferBase(GL_SHADER_STORAGE_BUFFER, B_SKIN, m.skin ? m.skin : empty_); boundModel = d.model; } mp.textures = d.textures; mp.tint = d.tint; mp.material = shadow ? 0 : d.material; mp.drawIndex = d.draw; mp.firstJoint = isSkinned ? d.firstJoint : 0; push(mp); gl::DrawElements(GL_TRIANGLES, GLsizei(d.indexCount), GL_UNSIGNED_INT, reinterpret_cast(uintptr_t(d.firstIndex) * 4)); } } void OpenGLBackend::shadowPasses(GLuint fbo, uint32_t size, const std::vector& passes) { gl::BindFramebuffer(GL_FRAMEBUFFER, fbo); gl::DepthMask(GL_TRUE); gl::Disable(GL_SCISSOR_TEST); const GLfloat one = 1.0f; gl::ClearNamedFramebufferfv(fbo, GL_DEPTH, 0, &one); gl::Enable(GL_SCISSOR_TEST); gl::Enable(GL_POLYGON_OFFSET_FILL); gl::PolygonOffset(2.5f, 2.0f); // same as Vulkan's depth bias (slope, constant) for (const ShadowPass& pass : passes) { gl::Viewport(pass.rect.x, pass.rect.y, pass.rect.z, pass.rect.w); gl::Scissor(pass.rect.x, pass.rect.y, pass.rect.z, pass.rect.w); drawMeshes(*pass.draws, pass.viewProj, true, shadowMeshProgram_, shadowSkinnedProgram_); } gl::Disable(GL_POLYGON_OFFSET_FILL); gl::Disable(GL_SCISSOR_TEST); (void)size; } void OpenGLBackend::render(FramePacket& p) { ++frameNumber_; ensureTargets(p.width, p.height); // the scene's size (render scale), not the window's std::erase_if(retiredTextures_, [&](auto& r) { if (frameNumber_ - r.first <= 3) return false; releaseTexture(r.second); return true; }); gl::ClipControl(GL_LOWER_LEFT, GL_ZERO_TO_ONE); // ImGui may have touched state; ours is set fresh each frame gl::FrontFace(GL_CW); auto upload = [](GLuint buffer, const void* data, size_t size) { if (size) gl::NamedBufferSubData(buffer, 0, GLsizeiptr(size), data); }; upload(draws_, p.draws.data(), p.draws.size() * sizeof(gpu::DrawData)); upload(joints_, p.joints.data(), p.joints.size() * sizeof(glm::mat4)); upload(frame_, &p.frame, sizeof p.frame); upload(lights_, p.lights.data(), p.lights.size() * sizeof(gpu::Light)); upload(views_, p.views.data(), p.views.size() * sizeof(gpu::ShadowView)); upload(cells_, p.clusters->cells.data(), p.clusters->cells.size() * sizeof(glm::uvec2)); upload(indices_, p.clusters->indices.data(), p.clusters->indices.size() * sizeof(uint32_t)); const std::pair bindings[] = {{B_DRAWS, draws_}, {B_JOINTS, joints_}, {B_FRAME, frame_}, {B_LIGHTS, lights_}, {B_CELLS, cells_}, {B_INDICES, indices_}, {B_VIEWS, views_}, {B_TEXTURES, textureTable_}, {B_SHADOW_TEXTURES, shadowTable_}}; for (auto [binding, buffer] : bindings) gl::BindBufferBase(GL_SHADER_STORAGE_BUFFER, binding, buffer); gl::BindBufferBase(GL_UNIFORM_BUFFER, 0, push_); gl::BindVertexArray(vao_); gl::Disable(GL_CULL_FACE); gl::Disable(GL_BLEND); gl::Enable(GL_DEPTH_TEST); gl::DepthFunc(GL_LESS); if (p.shadowSize != shadowMapSize_) { // r_shadowres changed releaseTexture(sunShadow_); gl::DeleteFramebuffers(1, &sunShadowFbo_); sunShadow_ = depthTarget(p.shadowSize, sunShadowFbo_); setHandles(kSunShadowSlot, sunShadow_, clampSampler_, true); shadowMapSize_ = p.shadowSize; } shadowPasses(sunShadowFbo_, p.shadowSize, p.sunPasses); // always: clears it when there's no sun if (!p.atlasPasses.empty()) shadowPasses(atlasFbo_, kAtlasSize, p.atlasPasses); // ── Mirror: the scene from the reflected eye, cut at the mirror, with its own frame data ── if (p.mirror) { gpu::FrameData mf = p.mirrorFrame; mf.gi = mf.giOrigin = glm::vec4(0.0f); // (no GI probes here) upload(mirrorFrame_, &mf, sizeof mf); gl::BindBufferBase(GL_SHADER_STORAGE_BUFFER, B_FRAME, mirrorFrame_); gl::BindFramebuffer(GL_FRAMEBUFFER, mirrorFbo_); gl::Viewport(0, 0, GLsizei(p.width), GLsizei(p.height)); gl::DepthMask(GL_TRUE); const GLfloat mclear[4] = {p.clearColor.r, p.clearColor.g, p.clearColor.b, 1.0f}, mone = 1.0f; gl::ClearNamedFramebufferfv(mirrorFbo_, GL_COLOR, 0, mclear); gl::ClearNamedFramebufferfv(mirrorFbo_, GL_DEPTH, 0, &mone); gl::Enable(GL_SCISSOR_TEST); // only where the mirror is on screen gl::Scissor(p.mirrorRect.x, p.mirrorRect.y, p.mirrorRect.z, p.mirrorRect.w); drawMeshes(p.mirrorOpaque, p.mirrorViewProj, false, meshProgram_, skinnedProgram_); if (p.frame.sky.x > 0.5f) { gl::UseProgram(skyProgram_); push(gpu::SkyPush{p.mirrorInvViewProj, p.frame.sunDir, p.frame.sunColor, p.frame.sky, glm::vec4(p.skyCloud, p.skyPhotoSunZ, p.frame.weather.z, p.frame.weather.w)}); gl::DepthFunc(GL_LEQUAL); gl::DepthMask(GL_FALSE); gl::DrawArrays(GL_TRIANGLES, 0, 3); gl::DepthMask(GL_TRUE); gl::DepthFunc(GL_LESS); } if (!p.mirrorTranslucent.empty()) { gl::Enable(GL_BLEND); gl::BlendFuncSeparate(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA, GL_ONE, GL_ONE_MINUS_SRC_ALPHA); gl::DepthMask(GL_FALSE); drawMeshes(p.mirrorTranslucent, p.mirrorViewProj, false, meshProgram_, skinnedProgram_); gl::DepthMask(GL_TRUE); gl::Disable(GL_BLEND); } gl::Disable(GL_SCISSOR_TEST); gl::BindBufferBase(GL_SHADER_STORAGE_BUFFER, B_FRAME, frame_); } // ── Scene pass (HDR) ── gl::BindFramebuffer(GL_FRAMEBUFFER, sceneFbo_); gl::Viewport(0, 0, GLsizei(p.width), GLsizei(p.height)); gl::DepthMask(GL_TRUE); const GLfloat clear[4] = {p.clearColor.r, p.clearColor.g, p.clearColor.b, 1.0f}, one = 1.0f; const GLfloat noReflection[4] = {0.5f, 0.5f, 1.0f, 0.0f}; gl::ClearNamedFramebufferfv(sceneFbo_, GL_COLOR, 0, clear); gl::ClearNamedFramebufferfv(sceneFbo_, GL_COLOR, 1, noReflection); gl::ClearNamedFramebufferfv(sceneFbo_, GL_DEPTH, 0, &one); // Only opaque meshes write the G-buffer (attachment 1). auto gbufWrites = [](bool on) { gl::ColorMaski(1, on, on, on, on); }; gbufWrites(false); gl::UseProgram(boxProgram_); for (const gpu::Box& b : p.boxes) { push(BoxPush{p.viewProj, b}); gl::DrawArrays(GL_TRIANGLES, 0, 36); } gbufWrites(true); drawMeshes(p.opaque, p.viewProj, false, meshProgram_, skinnedProgram_); gbufWrites(false); if (p.frame.sky.x > 0.5f) { // where nothing was drawn gl::UseProgram(skyProgram_); push(gpu::SkyPush{p.invViewProj, p.frame.sunDir, p.frame.sunColor, p.frame.sky, glm::vec4(p.skyCloud, p.skyPhotoSunZ, p.frame.weather.z, p.frame.weather.w)}); gl::DepthFunc(GL_LEQUAL); gl::DepthMask(GL_FALSE); gl::DrawArrays(GL_TRIANGLES, 0, 3); gl::DepthMask(GL_TRUE); gl::DepthFunc(GL_LESS); } if (!p.translucent.empty()) { // back to front, blended, no depth writes gl::Enable(GL_BLEND); gl::BlendFuncSeparate(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA, GL_ONE, GL_ONE_MINUS_SRC_ALPHA); gl::DepthMask(GL_FALSE); drawMeshes(p.translucent, p.viewProj, false, meshProgram_, skinnedProgram_); gl::DepthMask(GL_TRUE); gl::Disable(GL_BLEND); } if (!p.sprites.empty() || !p.glowSprites.empty()) { // particles: depth-tested, not written; blended, then added on gbufWrites(false); gl::UseProgram(spriteProgram_); gl::Enable(GL_BLEND); gl::DepthMask(GL_FALSE); gl::BlendFuncSeparate(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA, GL_ONE, GL_ONE_MINUS_SRC_ALPHA); for (const gpu::Sprite& sp : p.sprites) push(SpritePush{p.viewProj, sp}), gl::DrawArrays(GL_TRIANGLES, 0, 6); gl::BlendFuncSeparate(GL_SRC_ALPHA, GL_ONE, GL_ONE, GL_ONE); for (const gpu::Sprite& sp : p.glowSprites) push(SpritePush{p.viewProj, sp}), gl::DrawArrays(GL_TRIANGLES, 0, 6); gl::DepthMask(GL_TRUE); gl::Disable(GL_BLEND); gbufWrites(true); } gbufWrites(true); // ── Occlusion grid: the depth (only opaque things write it) shrunk and read back ── if (p.hzbTag) { gl::BindFramebuffer(GL_FRAMEBUFFER, hzbFbo_); gl::Viewport(0, 0, GLsizei(kHzbWidth), GLsizei(kHzbHeight)); gl::Disable(GL_DEPTH_TEST); gl::UseProgram(hzbProgram_); push(HzbPush{kDepthSlot, p.width, p.height, kHzbWidth, kHzbHeight, {}}); gl::DrawArrays(GL_TRIANGLES, 0, 3); const int i = hzbNext_; hzbNext_ = (hzbNext_ + 1) % kHzbRing; collectHzb(i, true); // the CPU ran ahead (no vsync): take that grid before the slot is reused gl::BindBuffer(GL_PIXEL_PACK_BUFFER, hzbPbo_[i]); gl::ReadBuffer(GL_COLOR_ATTACHMENT0); gl::ReadPixels(0, 0, GLsizei(kHzbWidth), GLsizei(kHzbHeight), GL_RED, GL_FLOAT, nullptr); gl::BindBuffer(GL_PIXEL_PACK_BUFFER, 0); hzbFence_[i] = gl::FenceSync(GL_SYNC_GPU_COMMANDS_COMPLETE, 0); hzbTags_[i] = p.hzbTag; gl::Enable(GL_DEPTH_TEST); } // ── Post: ambient occlusion, bloom ── gl::Disable(GL_DEPTH_TEST); auto pass = [&](const PostTarget& t, GLuint prog, const auto& data, bool additive) { gl::BindFramebuffer(GL_FRAMEBUFFER, t.fbo); gl::Viewport(0, 0, GLsizei(t.w), GLsizei(t.h)); gl::UseProgram(prog); push(data); if (additive) { gl::Enable(GL_BLEND); gl::BlendFuncSeparate(GL_ONE, GL_ONE, GL_ONE, GL_ONE); } gl::DrawArrays(GL_TRIANGLES, 0, 3); if (additive) gl::Disable(GL_BLEND); }; const bool ao = p.post.aoStrength > 0.0f, bloom = p.post.bloomStrength > 0.0f, reflections = p.post.ssr > 0.0f, volumetric = p.frame.volumetric.x > 0.0f, // the sun and the dynamic lights ssr = reflections || volumetric; // (the half-size target both go to) if (reflections) pass(ssr_, ssrProgram_, SsrPush{kDepthSlot, kGbufSlot, kSceneSlot, p.width, p.height, ssr_.w, ssr_.h, p.post.ssr, p.post.proj}, false); if (volumetric) // sunlight in the air, onto the same target pass(ssr_, volumetricProgram_, VolumetricPush{kDepthSlot, p.width, p.height, ssr_.w, ssr_.h, {}, p.post.proj}, reflections); TextureId resolved = kSceneSlot; if (p.post.taa) { taaCur_ ^= 1u; pass(taa_[taaCur_], taaProgram_, TaaPush{p.post.reproject, kSceneSlot, kTaaSlot + (taaCur_ ^ 1u), kDepthSlot, p.width, p.height, p.post.taaReset ? 1u : 0u, ssr ? kSsrSlot : 0u, 0}, false); resolved = kTaaSlot + taaCur_; } if (ao) { pass(ao_, ssaoProgram_, SsaoPush{kDepthSlot, p.width, p.height, ao_.w, ao_.h, p.post.aoRadius, p.post.aoStrength, 0, p.post.proj}, false); pass(aoBlur_, ssaoBlurProgram_, SsaoBlurPush{kAoSlot, kDepthSlot, p.width, p.height, aoBlur_.w, aoBlur_.h, 0, 0, p.post.proj}, false); } if (bloom) { for (uint32_t i = 0; i < kBloomLevels; ++i) { const uint32_t sw = i == 0 ? p.width : bloom_[i - 1].w, sh = i == 0 ? p.height : bloom_[i - 1].h; pass(bloom_[i], bloomDownProgram_, BloomPush{i == 0 ? resolved : kBloomSlot + i - 1, sw, sh, bloom_[i].w, bloom_[i].h, i == 0 ? 1u : 0u, {}}, false); } const float radius = 1.0f; uint32_t radiusBits; std::memcpy(&radiusBits, &radius, 4); for (uint32_t i = kBloomLevels - 1; i > 0; --i) pass(bloom_[i - 1], bloomUpProgram_, BloomPush{kBloomSlot + i, bloom_[i].w, bloom_[i].h, bloom_[i - 1].w, bloom_[i - 1].h, radiusBits, {}}, true); } if (p.post.adapt) { // eye adaptation from the smallest bloom level adaptCur_ ^= 1u; pass(adapt_[adaptCur_], adaptProgram_, AdaptPush{kBloomSlot + kBloomLevels - 1, p.post.adaptReset || !adaptValid_ ? 0u : kAdaptSlot + (adaptCur_ ^ 1u), p.post.adaptDt, 1.5f, p.post.adaptKey, 0.6f, 1.8f, 0}, false); adaptValid_ = true; } // ── Tonemap to the window, then UI ── gl::BindFramebuffer(GL_FRAMEBUFFER, 0); gl::Viewport(0, 0, GLsizei(p.outWidth), GLsizei(p.outHeight)); gl::Disable(GL_DEPTH_TEST); gl::UseProgram(tonemapProgram_); push(TonemapPush{resolved, p.exposure, ao ? kAoBlurSlot : 0u, bloom ? kBloomSlot : 0u, p.post.bloomStrength, p.post.saturation, p.post.vignette, p.post.taa ? p.post.sharpen : 0.0f, kDepthSlot, {ssr && !p.post.taa ? kSsrSlot : 0u, p.post.adapt ? kAdaptSlot + adaptCur_ : 0u, 0}, p.post.shafts, p.post.sunColor}); gl::DrawArrays(GL_TRIANGLES, 0, 3); gl::BindVertexArray(0); ImGui_ImplOpenGL3_RenderDrawData(ImGui::GetDrawData()); if (!p.screenshot.empty()) { std::vector rows(size_t(p.outWidth) * p.outHeight * 4), px(rows.size()); gl::ReadBuffer(GL_BACK); gl::ReadPixels(0, 0, GLsizei(p.outWidth), GLsizei(p.outHeight), GL_RGBA, GL_UNSIGNED_BYTE, rows.data()); const size_t stride = size_t(p.outWidth) * 4; // the window's row 0 is its bottom for (uint32_t y = 0; y < p.outHeight; ++y) std::memcpy(&px[y * stride], &rows[(p.outHeight - 1 - y) * stride], stride); saveScreenshot(p.screenshot, std::move(px), p.outWidth, p.outHeight); } SDL_GL_SwapWindow(window_); } } // namespace std::unique_ptr createOpenGLBackend() { return std::make_unique(); } } // namespace keel