Fry3DX 0.4: an easy 3D modeller

Shapes with live settings, shape editing, knife and hole tools, drawing
shapes onto other objects, combining objects (add / subtract / overlap),
copies, thickness, painting with stickers and layers, a Layout tab, a
Print tab with checks, packed .fry files with autosave, export to GLB,
glTF, FBX, OBJ, 3MF, STL and PLY, folders in Layers, its own crash
screen, and a website for GitHub Pages in docs/.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NMSrd5oe1GpUD7E6d825KP
This commit is contained in:
Nexoniarz 2026-10-19 21:18:35 +02:00
commit f3e8eca87b
135 changed files with 23423 additions and 0 deletions

View file

@ -0,0 +1,858 @@
// 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 "render/backend.h"
#include "render/screenshot.h"
#include "rhi/gl/gl_loader.h"
#include "core/log.h"
#include "platform/window.h"
#include <SDL3/SDL.h>
#include <imgui.h>
#include <imgui_impl_opengl3.h>
#include <imgui_impl_sdl3.h>
#include <cstring>
#include <map>
#include <string>
#include <string_view>
namespace keel {
namespace {
// Every shader's source and the shared headers, embedded by the build (cmake/EmbedText.cmake).
const std::map<std::string_view, std::string_view>& shaderSources() {
static const std::map<std::string_view, std::string_view> sources = [] {
struct Entry { const char* name; const char* text; };
static const Entry entries[] = {
#include "gl_sources.inc"
};
std::map<std::string_view, std::string_view> 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<const char*>(gl::GetStringi(GL_EXTENSIONS, GLuint(i)))) == "GL_ARB_bindless_texture";
log::info("GPU: {} ({})", reinterpret_cast<const char*>(gl::GetString(GL_RENDERER)), reinterpret_cast<const char*>(gl::GetString(GL_VERSION)));
gpu_.name = reinterpret_cast<const char*>(gl::GetString(GL_RENDERER)), gpu_.driver = reinterpret_cast<const char*>(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, &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_}) 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();
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<std::vector<uint8_t>>& 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());
}
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);
}
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<const float*>(gl::MapNamedBufferRange(hzbPbo_[i], 0, kHzbWidth * kHzbHeight * 4, GL_MAP_READ_BIT))) {
hzbLatest_ = {hzbTags_[i], std::vector<float>(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;
};
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 <typename T> void push(const T& data) { gl::NamedBufferSubData(push_, 0, sizeof data, &data); }
void drawMeshes(const std::vector<MeshDraw>& draws, const glm::mat4& viewProj, bool shadow, GLuint mesh, GLuint skinned);
void shadowPasses(GLuint fbo, uint32_t size, const std::vector<ShadowPass>& passes);
SDL_Window* window_ = nullptr;
SDL_GLContext context_ = nullptr;
bool imgui_ = false;
GLuint repeatSampler_ = 0, clampSampler_ = 0, compareSampler_ = 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<std::pair<uint64_t, Texture>> 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<TextureId, Texture> textures_;
std::vector<Model> models_;
};
void OpenGLBackend::drawMeshes(const std::vector<MeshDraw>& 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<const void*>(uintptr_t(d.firstIndex) * 4));
}
}
void OpenGLBackend::shadowPasses(GLuint fbo, uint32_t size, const std::vector<ShadowPass>& 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<Binding, GLuint> 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<uint8_t> 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<RenderBackend> createOpenGLBackend() { return std::make_unique<OpenGLBackend>(); }
} // namespace keel

820
engine/rhi/gl/gl_legacy.cpp Normal file
View file

@ -0,0 +1,820 @@
// The fallback renderer for old GPUs: OpenGL 4.0 or 3.1, GLSL 1.40, nothing newer — bound
// textures, vertex attributes, one uniform block for the frame, a texture buffer for skinning.
// Two tiers, cut down from the main renderer:
// 4.0 lightmaps, sun with cascaded shadows (PCF), HDR sky, fog, normal maps, GGX highlights,
// sky reflections, the 16 nearest dynamic lights (no shadows)
// 3.1 lightmaps, sun with shadows (one tap), sky, fog; flat surfaces, no highlights or lights
// Neither has clustered lights, light shadows or occlusion culling. Depth is GL's -1..1 here
// (no glClipControl), so vertex shaders turn the 0..1 matrices' z into it.
#include "render/backend.h"
#include "core/log.h"
#include "platform/window.h"
#include "render/screenshot.h"
#include <SDL3/SDL.h>
#include <SDL3/SDL_opengl.h>
#include <glm/gtc/packing.hpp>
#include <glm/gtc/type_ptr.hpp>
#include <imgui.h>
#include <imgui_impl_opengl3.h>
#include <imgui_impl_sdl3.h>
#include <algorithm>
#include <cstddef>
#include <cstring>
#include <string>
#include <vector>
namespace keel {
namespace {
// Entry points from OpenGL 3.1 (no DSA, no sampler objects, no storage buffers).
#define KEEL_GLX_FUNCTIONS(X) \
X(const GLubyte*, GetString, (GLenum)) \
X(void, GetIntegerv, (GLenum, GLint*)) \
X(void, Enable, (GLenum)) \
X(void, Disable, (GLenum)) \
X(void, Viewport, (GLint, GLint, GLsizei, GLsizei)) \
X(void, Clear, (GLbitfield)) \
X(void, ClearColor, (GLfloat, GLfloat, GLfloat, GLfloat)) \
X(void, ClearDepth, (GLdouble)) \
X(void, DepthFunc, (GLenum)) \
X(void, DepthMask, (GLboolean)) \
X(void, BlendFuncSeparate, (GLenum, GLenum, GLenum, GLenum)) \
X(void, PolygonOffset, (GLfloat, GLfloat)) \
X(void, PixelStorei, (GLenum, GLint)) \
X(void, DrawBuffer, (GLenum)) \
X(void, ReadBuffer, (GLenum)) \
X(void, ReadPixels, (GLint, GLint, GLsizei, GLsizei, GLenum, GLenum, void*)) \
X(void, GenBuffers, (GLsizei, GLuint*)) \
X(void, DeleteBuffers, (GLsizei, const GLuint*)) \
X(void, BindBuffer, (GLenum, GLuint)) \
X(void, BindBufferBase, (GLenum, GLuint, GLuint)) \
X(void, BufferData, (GLenum, GLsizeiptr, const void*, GLenum)) \
X(void, BufferSubData, (GLenum, GLintptr, GLsizeiptr, const void*)) \
X(void, GenTextures, (GLsizei, GLuint*)) \
X(void, DeleteTextures, (GLsizei, const GLuint*)) \
X(void, BindTexture, (GLenum, GLuint)) \
X(void, ActiveTexture, (GLenum)) \
X(void, TexImage2D, (GLenum, GLint, GLint, GLsizei, GLsizei, GLint, GLenum, GLenum, const void*)) \
X(void, TexParameteri, (GLenum, GLenum, GLint)) \
X(void, TexBuffer, (GLenum, GLenum, GLuint)) \
X(void, GenFramebuffers, (GLsizei, GLuint*)) \
X(void, DeleteFramebuffers, (GLsizei, const GLuint*)) \
X(void, BindFramebuffer, (GLenum, GLuint)) \
X(void, FramebufferTexture2D, (GLenum, GLenum, GLenum, GLuint, GLint)) \
X(GLenum, CheckFramebufferStatus, (GLenum)) \
X(void, GenVertexArrays, (GLsizei, GLuint*)) \
X(void, DeleteVertexArrays, (GLsizei, const GLuint*)) \
X(void, BindVertexArray, (GLuint)) \
X(void, EnableVertexAttribArray, (GLuint)) \
X(void, VertexAttribPointer, (GLuint, GLint, GLenum, GLboolean, GLsizei, const void*)) \
X(void, VertexAttribIPointer, (GLuint, GLint, GLenum, GLsizei, const void*)) \
X(void, VertexAttribI4ui, (GLuint, GLuint, GLuint, GLuint, GLuint)) \
X(void, VertexAttrib4f, (GLuint, GLfloat, GLfloat, GLfloat, GLfloat)) \
X(void, DrawElements, (GLenum, GLsizei, GLenum, const void*)) \
X(void, DrawArrays, (GLenum, GLint, GLsizei)) \
X(GLuint, CreateShader, (GLenum)) \
X(void, ShaderSource, (GLuint, GLsizei, const GLchar* const*, const GLint*)) \
X(void, CompileShader, (GLuint)) \
X(void, GetShaderiv, (GLuint, GLenum, GLint*)) \
X(void, GetShaderInfoLog, (GLuint, GLsizei, GLsizei*, GLchar*)) \
X(void, DeleteShader, (GLuint)) \
X(GLuint, CreateProgram, (void)) \
X(void, AttachShader, (GLuint, GLuint)) \
X(void, BindAttribLocation, (GLuint, GLuint, const GLchar*)) \
X(void, BindFragDataLocation, (GLuint, GLuint, const GLchar*)) \
X(void, LinkProgram, (GLuint)) \
X(void, GetProgramiv, (GLuint, GLenum, GLint*)) \
X(void, GetProgramInfoLog, (GLuint, GLsizei, GLsizei*, GLchar*)) \
X(void, DeleteProgram, (GLuint)) \
X(void, UseProgram, (GLuint)) \
X(GLint, GetUniformLocation, (GLuint, const GLchar*)) \
X(GLuint, GetUniformBlockIndex, (GLuint, const GLchar*)) \
X(void, UniformBlockBinding, (GLuint, GLuint, GLuint)) \
X(void, Uniform1i, (GLint, GLint)) \
X(void, Uniform4fv, (GLint, GLsizei, const GLfloat*)) \
X(void, UniformMatrix4fv, (GLint, GLsizei, GLboolean, const GLfloat*))
namespace glx {
#define KEEL_GLX_DECLARE(ret, name, params) using name##_t = ret(APIENTRY*) params; name##_t name = nullptr;
KEEL_GLX_FUNCTIONS(KEEL_GLX_DECLARE)
#undef KEEL_GLX_DECLARE
bool load() {
std::string missing;
#define KEEL_GLX_LOAD(ret, name, params) \
name = reinterpret_cast<name##_t>(SDL_GL_GetProcAddress("gl" #name)); \
if (!name) missing += " gl" #name;
KEEL_GLX_FUNCTIONS(KEEL_GLX_LOAD)
#undef KEEL_GLX_LOAD
if (!missing.empty()) log::warn("OpenGL (legacy): missing functions:{}", missing);
return missing.empty();
}
} // namespace glx
// ── Shaders (GLSL 1.40; TIER is 1 for GL 3.1, 2 for 4.0) ──
const char* kFrameBlock = R"(
layout(std140) uniform Frame { // (GLSL 1.40: no instance name, so the members say F_)
mat4 F_cascades[4];
vec4 F_splits, F_cascadeTexel, F_sunDir, F_sunColor, F_shadow, F_camPos, F_sky, F_fog, F_lightCount;
vec4 F_lights[64]; // 16 × (pos radius, colour, spot dir cos-outer, cos-inner)
};
)";
const char* kMeshVert = R"(
in vec3 a_pos; in vec3 a_normal; in vec2 a_uv; in vec2 a_lmuv; in uvec4 a_joints; in vec4 a_weights;
uniform mat4 u_viewProj, u_model;
uniform int u_skinned, u_firstJoint;
uniform samplerBuffer u_joints;
out vec3 v_normal, v_world; out vec2 v_uv, v_lmuv;
mat4 joint(uint j) {
int b = (u_firstJoint + int(j)) * 4;
return mat4(texelFetch(u_joints, b), texelFetch(u_joints, b + 1), texelFetch(u_joints, b + 2), texelFetch(u_joints, b + 3));
}
void main() {
mat4 m = u_model;
if (u_skinned != 0)
m = joint(a_joints.x) * a_weights.x + joint(a_joints.y) * a_weights.y + joint(a_joints.z) * a_weights.z + joint(a_joints.w) * a_weights.w;
vec4 w = m * vec4(a_pos, 1.0);
v_world = w.xyz;
v_normal = mat3(m) * a_normal;
v_uv = a_uv;
v_lmuv = a_lmuv;
gl_Position = u_viewProj * w;
gl_Position.z = gl_Position.z * 2.0 - gl_Position.w; // 0..1 → GL's -1..1
}
)";
const char* kDepthFrag = R"(
out vec4 o_color;
void main() { o_color = vec4(1.0); }
)";
const char* kMeshFrag = R"(
in vec3 v_normal, v_world; in vec2 v_uv, v_lmuv;
uniform sampler2D u_albedo, u_lightmap, u_material, u_skyVis, u_sky;
uniform sampler2DShadow u_sunShadow;
uniform vec4 u_tint, u_params, u_plane, u_flags; // flags: x lightmap, y material, z sky visibility
uniform vec4 u_cube[6];
out vec4 o_color;
vec3 ambientCube(vec3 n) {
vec3 w = n * n;
return w.x * (n.x > 0.0 ? u_cube[0].rgb : u_cube[1].rgb) + w.y * (n.y > 0.0 ? u_cube[2].rgb : u_cube[3].rgb)
+ w.z * (n.z > 0.0 ? u_cube[4].rgb : u_cube[5].rgb);
}
float cascadeShadow(vec3 n, vec3 fwd) {
if (F_sunColor.w < 0.5) return 1.0;
float depth = dot(v_world - F_camPos.xyz, fwd);
if (depth > F_splits.w) return 1.0;
int c = depth < F_splits.x ? 0 : depth < F_splits.y ? 1 : depth < F_splits.z ? 2 : 3;
vec4 p = F_cascades[c] * vec4(v_world + n * F_cascadeTexel[c] * 1.5, 1.0);
vec2 uv = p.xy * 0.5 + 0.5;
if (any(lessThan(uv, vec2(0.0))) || any(greaterThan(uv, vec2(1.0))) || p.z > 1.0) return 1.0;
vec2 base = vec2(c & 1, c >> 1) * 0.5;
vec2 at = base + uv * 0.5;
float t = F_shadow.x;
#if TIER >= 2
float lit = 0.0;
for (int y = 0; y < 2; ++y)
for (int x = 0; x < 2; ++x) lit += texture(u_sunShadow, vec3(clamp(at + (vec2(x, y) - 0.5) * t, base + t, base + 0.5 - t), p.z - 0.0002));
return lit * 0.25;
#else
return texture(u_sunShadow, vec3(at, p.z - 0.0003));
#endif
}
vec3 sky(vec3 dir, float level) {
float c = cos(F_sky.w), s = sin(F_sky.w);
dir = vec3(c * dir.x + s * dir.y, -s * dir.x + c * dir.y, dir.z);
vec4 m = textureLod(u_sky, vec2(atan(dir.y, dir.x) * 0.15915494 + 0.5, acos(clamp(dir.z, -1.0, 1.0)) * 0.31830989), level);
return m.rgb * m.a * 16.0 * F_sky.y;
}
#if TIER >= 2
mat3 cotangentFrame(vec3 n, vec3 p, vec2 uv) {
vec3 dp1 = dFdx(p), dp2 = dFdy(p);
vec2 duv1 = dFdx(uv), duv2 = dFdy(uv);
vec3 dp2perp = cross(dp2, n), dp1perp = cross(n, dp1);
vec3 t = dp2perp * duv1.x + dp1perp * duv2.x, b = dp2perp * duv1.y + dp1perp * duv2.y;
float invmax = inversesqrt(max(max(dot(t, t), dot(b, b)), 1e-20));
return mat3(t * invmax, b * invmax, n);
}
vec3 g_f0 = vec3(0.04); // reflectance at normal incidence: 0.04, or a metal's albedo
vec3 specGGX(vec3 n, vec3 l, vec3 v, float rough) {
vec3 h = normalize(l + v);
float a = max(rough * rough, 0.002), a2 = a * a;
float ndl = max(dot(n, l), 1e-4), ndv = max(dot(n, v), 1e-4), ndh = max(dot(n, h), 0.0), vdh = max(dot(v, h), 0.0);
float q = ndh * ndh * (a2 - 1.0) + 1.0;
float vis = 0.5 / (ndl * sqrt(ndv * ndv * (1.0 - a2) + a2) + ndv * sqrt(ndl * ndl * (1.0 - a2) + a2));
return min(a2 / (3.14159265 * q * q) * vis * (g_f0 + (1.0 - g_f0) * pow(1.0 - vdh, 5.0)) * 3.14159265, vec3(64.0));
}
vec2 envBRDF(float rough, float ndv) {
vec4 r = rough * vec4(-1.0, -0.0275, -0.572, 0.022) + vec4(1.0, 0.0425, 1.04, -0.04);
float a004 = min(r.x * r.x, exp2(-9.28 * ndv)) * r.x + r.y;
return vec2(-1.04, 1.04) * a004 + r.zw;
}
#endif
void main() {
vec4 albedo = texture(u_albedo, v_uv);
if (albedo.a < 0.5) discard;
if (u_params.z > 0.5 && dot(v_world, u_plane.xyz) > u_plane.w) discard; // liquid cut
vec3 base = albedo.rgb * u_tint.rgb;
vec3 n = normalize(v_normal), geo = n;
vec3 view = normalize(F_camPos.xyz - v_world);
vec3 fwd = F_lightCount.yzw; // the camera's forward (packed here)
float gloss = 0.15;
#if TIER >= 2
mat3 tbn = cotangentFrame(n, v_world, v_uv);
if (u_flags.y > 0.5) {
vec4 m = texture(u_material, v_uv);
vec2 xy = m.xy * 2.0 - 1.0; // Z rebuilt; B is metalness
n = normalize(tbn * vec3(xy, sqrt(max(1.0 - dot(xy, xy), 0.0))));
gloss = m.w;
g_f0 = mix(vec3(0.04), base, m.z);
base *= 1.0 - m.z;
}
#endif
vec3 sunDir = F_sunDir.xyz;
float facing = dot(geo, sunDir) > 0.0 ? max(dot(n, sunDir), 0.0) : 0.0;
vec3 sun = F_sunColor.rgb * F_sunColor.w * facing;
float vis = facing > 0.0 ? cascadeShadow(geo, fwd) : 0.0;
bool realtime = F_shadow.z > 0.5;
vec3 light, env = vec3(0.0);
float skyVis = 0.0;
if (u_flags.x > 0.5) {
vec4 rgbm = texture(u_lightmap, v_lmuv);
env = rgbm.rgb * rgbm.a * 8.0;
light = realtime ? env + sun * vis : env * (1.0 - F_shadow.y * (1.0 - vis) * facing * F_sunColor.w);
if (u_flags.z > 0.5) skyVis = texture(u_skyVis, v_lmuv).r;
} else if (u_params.y > 0.5) {
env = ambientCube(n);
light = env + sun * (realtime ? vis : u_params.x * vis);
skyVis = 0.5 * u_params.x;
} else {
light = vec3(0.3 + 0.9 * max(dot(n, normalize(vec3(0.4, 0.3, 0.85))), 0.0));
}
vec3 color = base * light;
#if TIER >= 2
float rough = clamp(1.0 - gloss, 0.05, 1.0);
color += F_sunColor.rgb * F_sunColor.w * facing * vis * specGGX(n, sunDir, view, rough);
for (int i = 0; i < int(F_lightCount.x); ++i) {
vec4 pr = F_lights[i * 4], lc = F_lights[i * 4 + 1], sp = F_lights[i * 4 + 2], ex = F_lights[i * 4 + 3];
vec3 toLight = pr.xyz - v_world;
float dist = length(toLight);
if (dist >= pr.w) continue;
vec3 L = toLight / max(dist, 0.001);
float ndl = dot(n, L);
if (ndl <= 0.0) continue;
float a = 1.0 - dist / pr.w, atten = a * a;
if (sp.w > -1.5) atten *= smoothstep(sp.w, ex.x, dot(-L, sp.xyz));
color += lc.rgb * atten * ndl * (base + specGGX(n, L, view, rough));
}
if (F_sky.x > 0.5) {
vec3 r = reflect(-view, n);
vec2 ab = envBRDF(rough, clamp(dot(n, view), 1e-4, 1.0));
color += mix(env, sky(r, rough * F_sky.z), skyVis * skyVis) * (g_f0 * ab.x + ab.y);
}
#endif
float alpha = u_tint.a;
if (alpha < 0.5) { // glass: mostly what's behind, a little reflection
float fresnel = pow(1.0 - clamp(abs(dot(n, view)), 0.0, 1.0), 4.0);
color = color * 0.12 + (F_sky.x > 0.5 ? sky(reflect(-view, n), 1.0) : vec3(0.45, 0.6, 0.8)) * fresnel * 0.8;
alpha = clamp(alpha * 0.6 + fresnel * 0.5, 0.0, 1.0);
}
if (F_sky.x > 0.5 && F_fog.x > 0.0) { // height fog coloured by the sky
vec3 ray = v_world - F_camPos.xyz;
float dist = length(ray);
vec3 rd = ray / max(dist, 1e-3);
float k = F_fog.y, kz = k * rd.z;
float amount = F_fog.x * exp(-k * (F_camPos.z - F_fog.w)) * (abs(kz) > 1e-6 ? (1.0 - exp(-kz * dist)) / kz : dist);
color = mix(color, sky(normalize(vec3(rd.xy, max(rd.z, 0.05))), F_sky.z) * 0.9, min(1.0 - exp(-amount), F_fog.z));
}
o_color = vec4(color, alpha);
}
)";
const char* kSkyVert = R"(
out vec2 v_ndc;
void main() {
vec2 uv = vec2((gl_VertexID << 1) & 2, gl_VertexID & 2);
v_ndc = uv * 2.0 - 1.0;
gl_Position = vec4(v_ndc, 1.0, 1.0);
}
)";
const char* kSkyFrag = R"(
in vec2 v_ndc;
uniform mat4 u_invViewProj;
uniform sampler2D u_sky;
out vec4 o_color;
void main() {
vec4 far = u_invViewProj * vec4(v_ndc, 1.0, 1.0), near = u_invViewProj * vec4(v_ndc, 0.0, 1.0);
vec3 dir = normalize(far.xyz / far.w - near.xyz / near.w);
float c = cos(F_sky.w), s = sin(F_sky.w);
vec3 d = vec3(c * dir.x + s * dir.y, -s * dir.x + c * dir.y, dir.z);
vec4 m = textureLod(u_sky, vec2(atan(d.y, d.x) * 0.15915494 + 0.5, acos(clamp(d.z, -1.0, 1.0)) * 0.31830989), 0.0);
vec3 col = m.rgb * m.a * 16.0 * F_sky.y;
if (F_sunColor.w > 0.5) {
float k = dot(dir, F_sunDir.xyz);
col += F_sunColor.rgb * (smoothstep(0.99995, 0.99999, k) * 60.0 + pow(max(k, 0.0), 600.0) * 1.5 + pow(max(k, 0.0), 40.0) * 0.08);
}
o_color = vec4(col, 1.0);
}
)";
const char* kBoxVert = R"(
uniform mat4 u_viewProj;
uniform vec4 u_center, u_extents;
out vec3 v_normal;
const vec3 kNormals[6] = vec3[](vec3(1,0,0), vec3(-1,0,0), vec3(0,1,0), vec3(0,-1,0), vec3(0,0,1), vec3(0,0,-1));
const vec2 kQuad[6] = vec2[](vec2(-1,-1), vec2(1,-1), vec2(1,1), vec2(-1,-1), vec2(1,1), vec2(-1,1));
void main() {
vec3 n = kNormals[gl_VertexID / 6];
vec3 t = abs(n.z) > 0.5 ? vec3(1,0,0) : vec3(0,0,1);
vec2 q = kQuad[gl_VertexID % 6];
vec3 w = u_center.xyz + (n + t * q.x + cross(n, t) * q.y) * u_extents.xyz;
v_normal = n;
gl_Position = u_viewProj * vec4(w, 1.0);
gl_Position.z = gl_Position.z * 2.0 - gl_Position.w;
}
)";
const char* kBoxFrag = R"(
in vec3 v_normal;
uniform vec4 u_color;
out vec4 o_color;
void main() { o_color = vec4(u_color.rgb * (0.5 + 0.5 * max(dot(v_normal, normalize(vec3(0.4, 0.3, 0.85))), 0.0)), u_color.a); }
)";
const char* kTonemapVert = R"(
out vec2 v_uv;
void main() {
v_uv = vec2((gl_VertexID << 1) & 2, gl_VertexID & 2);
gl_Position = vec4(v_uv * 2.0 - 1.0, 0.0, 1.0);
v_uv.y = 1.0 - v_uv.y; // the scene's row 0 is the view's top (as in the main renderer)
}
)";
const char* kTonemapFrag = R"(
in vec2 v_uv;
uniform sampler2D u_scene;
uniform vec4 u_exposure;
out vec4 o_color;
void main() {
vec3 x = texture(u_scene, v_uv).rgb * u_exposure.x;
x = clamp((x * (2.51 * x + 0.03)) / (x * (2.43 * x + 0.59) + 0.14), 0.0, 1.0);
o_color = vec4(mix(x * 12.92, 1.055 * pow(x, vec3(1.0 / 2.4)) - 0.055, step(0.0031308, x)), 1.0);
}
)";
// What the Frame block holds (std140: matches the GLSL above).
struct FrameBlock {
glm::mat4 cascades[4];
glm::vec4 splits, cascadeTexel, sunDir, sunColor, shadow, camPos, sky, fog, lightCount;
glm::vec4 lights[64];
};
constexpr int kMaxLegacyLights = 16;
struct Program {
GLuint id = 0;
GLint viewProj = -1, model = -1, skinned = -1, firstJoint = -1, tint = -1, params = -1, plane = -1, flags = -1, cube = -1,
invViewProj = -1, center = -1, extents = -1, color = -1, exposure = -1;
};
class LegacyGLBackend final : public RenderBackend {
public:
explicit LegacyGLBackend(int major, int minor) : major_(major), minor_(minor), tier_(major >= 4 ? 2 : 1) {
name_ = "OpenGL " + std::to_string(major) + "." + std::to_string(minor) + " (fallback)";
}
const char* name() const override { return name_.c_str(); }
GpuInfo gpuInfo() const override { return gpu_; }
GpuInfo gpu_;
bool init(const Window& window, bool) override {
window_ = window.handle();
SDL_GL_SetAttribute(SDL_GL_CONTEXT_MAJOR_VERSION, major_);
SDL_GL_SetAttribute(SDL_GL_CONTEXT_MINOR_VERSION, minor_);
// 3.1 has no profiles; 4.0 asks for core.
SDL_GL_SetAttribute(SDL_GL_CONTEXT_PROFILE_MASK, major_ >= 4 ? SDL_GL_CONTEXT_PROFILE_CORE : 0);
SDL_GL_SetAttribute(SDL_GL_CONTEXT_FLAGS, major_ >= 4 ? 0 : SDL_GL_CONTEXT_FORWARD_COMPATIBLE_FLAG);
SDL_GL_SetAttribute(SDL_GL_DOUBLEBUFFER, 1);
SDL_GL_SetAttribute(SDL_GL_DEPTH_SIZE, 0);
context_ = SDL_GL_CreateContext(window_);
if (!context_) {
log::warn("OpenGL {}.{} context: {}", major_, minor_, SDL_GetError());
return false;
}
SDL_GL_MakeCurrent(window_, context_);
if (!glx::load()) return false;
log::info("GPU: {} ({})", reinterpret_cast<const char*>(glx::GetString(GL_RENDERER)), reinterpret_cast<const char*>(glx::GetString(GL_VERSION)));
gpu_.name = reinterpret_cast<const char*>(glx::GetString(GL_RENDERER)), gpu_.driver = reinterpret_cast<const char*>(glx::GetString(GL_VERSION));
glx::PixelStorei(GL_UNPACK_ALIGNMENT, 1);
glx::PixelStorei(GL_PACK_ALIGNMENT, 1);
const std::string header = "#version 140\n#define TIER " + std::to_string(tier_) + "\n" + kFrameBlock;
mesh_ = link(header + kMeshVert, header + kMeshFrag);
depth_ = link(header + kMeshVert, header + kDepthFrag);
sky_ = link(header + kSkyVert, header + kSkyFrag);
box_ = link(header + kBoxVert, header + kBoxFrag);
tonemap_ = link(header + kTonemapVert, header + kTonemapFrag);
if (!mesh_.id || !depth_.id || !sky_.id || !box_.id || !tonemap_.id) return false;
glx::GenBuffers(1, &frameUbo_);
glx::BindBuffer(GL_UNIFORM_BUFFER, frameUbo_);
glx::BufferData(GL_UNIFORM_BUFFER, sizeof(FrameBlock), nullptr, GL_DYNAMIC_DRAW);
glx::GenBuffers(1, &jointBuffer_);
glx::BindBuffer(GL_TEXTURE_BUFFER, jointBuffer_);
glx::BufferData(GL_TEXTURE_BUFFER, kMaxJointsPerFrame * sizeof(glm::mat4), nullptr, GL_DYNAMIC_DRAW);
glx::GenTextures(1, &jointTexture_);
glx::BindTexture(GL_TEXTURE_BUFFER, jointTexture_);
glx::TexBuffer(GL_TEXTURE_BUFFER, GL_RGBA32F, jointBuffer_);
glx::GenVertexArrays(1, &emptyVao_);
// The sun's shadow map: 2×2 cascades, smaller on the 3.1 tier.
shadowSize_ = tier_ >= 2 ? kShadowSize : kShadowSize / 2;
glx::GenTextures(1, &shadowMap_);
glx::BindTexture(GL_TEXTURE_2D, shadowMap_);
glx::TexImage2D(GL_TEXTURE_2D, 0, GL_DEPTH_COMPONENT32F, GLsizei(shadowSize_), GLsizei(shadowSize_), 0, GL_DEPTH_COMPONENT, GL_FLOAT, nullptr);
glx::TexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
glx::TexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
glx::TexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
glx::TexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);
glx::TexParameteri(GL_TEXTURE_2D, GL_TEXTURE_COMPARE_MODE, GL_COMPARE_REF_TO_TEXTURE);
glx::TexParameteri(GL_TEXTURE_2D, GL_TEXTURE_COMPARE_FUNC, GL_LEQUAL);
glx::GenFramebuffers(1, &shadowFbo_);
glx::BindFramebuffer(GL_FRAMEBUFFER, shadowFbo_);
glx::FramebufferTexture2D(GL_FRAMEBUFFER, GL_DEPTH_ATTACHMENT, GL_TEXTURE_2D, shadowMap_, 0);
glx::DrawBuffer(GL_NONE);
glx::ReadBuffer(GL_NONE);
glx::BindFramebuffer(GL_FRAMEBUFFER, 0);
IMGUI_CHECKVERSION();
ImGui::CreateContext();
ImGui::GetIO().IniFilename = nullptr;
ImGui::StyleColorsDark();
imgui_ = ImGui_ImplSDL3_InitForOpenGL(window_, context_) && ImGui_ImplOpenGL3_Init("#version 140");
log::info("Fallback renderer: tier {} ({})", tier_, tier_ >= 2 ? "shadows, normal maps, highlights, sky reflections, 16 lights" : "lightmaps, sun shadows, sky");
return imgui_;
}
void shutdown() override {
if (!context_) return;
if (imgui_) {
ImGui_ImplOpenGL3_Shutdown();
ImGui_ImplSDL3_Shutdown();
ImGui::DestroyContext();
imgui_ = false;
}
if (glx::DeleteTextures) {
for (GLuint t : textures_)
if (t) glx::DeleteTextures(1, &t);
for (Model& m : models_) releaseModel(m);
releaseTargets();
for (GLuint* t : {&shadowMap_, &jointTexture_}) glx::DeleteTextures(1, t);
glx::DeleteFramebuffers(1, &shadowFbo_);
for (GLuint* b : {&frameUbo_, &jointBuffer_}) glx::DeleteBuffers(1, b);
glx::DeleteVertexArrays(1, &emptyVao_);
for (Program* p : {&mesh_, &depth_, &sky_, &box_, &tonemap_}) glx::DeleteProgram(p->id);
}
SDL_GL_DestroyContext(context_);
context_ = nullptr;
}
void beginUi() override {
ImGui_ImplOpenGL3_NewFrame();
ImGui_ImplSDL3_NewFrame();
ImGui::NewFrame();
}
void setVsync(bool on) override { SDL_GL_SetSwapInterval(on ? 1 : 0); }
void onResize() override {}
// No block compression here (old GPUs): the renderer decodes BC7 to RGBA8 first.
void createTexture(TextureId slot, uint32_t w, uint32_t h, bool srgb, const std::vector<std::vector<uint8_t>>& mips,
TexFormat = TexFormat::RGBA8) override {
if (slot >= textures_.size()) textures_.resize(slot + 1, 0);
GLuint& t = textures_[slot];
if (!t) glx::GenTextures(1, &t);
glx::BindTexture(GL_TEXTURE_2D, t);
for (size_t l = 0; l < mips.size(); ++l)
glx::TexImage2D(GL_TEXTURE_2D, GLint(l), srgb ? GL_SRGB8_ALPHA8 : GL_RGBA8, GLsizei(std::max(1u, w >> l)), GLsizei(std::max(1u, h >> l)), 0, GL_RGBA,
GL_UNSIGNED_BYTE, mips[l].data());
glx::TexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAX_LEVEL, GLint(mips.size()) - 1);
glx::TexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, mips.size() > 1 ? GL_LINEAR_MIPMAP_LINEAR : GL_LINEAR);
glx::TexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
glx::TexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_REPEAT);
glx::TexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_REPEAT);
}
void uploadModel(uint32_t index, const KmdlModel& kmdl) override {
if (index >= models_.size()) models_.resize(index + 1);
releaseModel(models_[index]);
Model& m = models_[index];
glx::GenVertexArrays(1, &m.vao);
glx::BindVertexArray(m.vao);
glx::GenBuffers(1, &m.vertices);
glx::BindBuffer(GL_ARRAY_BUFFER, m.vertices);
glx::BufferData(GL_ARRAY_BUFFER, GLsizeiptr(kmdl.vertices.size() * sizeof(KmdlVertex)), kmdl.vertices.data(), GL_STATIC_DRAW);
const GLsizei stride = sizeof(KmdlVertex);
const struct { GLuint loc; GLint n; size_t off; } attrs[] = {{0, 3, offsetof(KmdlVertex, position)}, {1, 3, offsetof(KmdlVertex, normal)},
{2, 2, offsetof(KmdlVertex, uv)}, {3, 2, offsetof(KmdlVertex, lightmapUv)}};
for (const auto& a : attrs) {
glx::EnableVertexAttribArray(a.loc);
glx::VertexAttribPointer(a.loc, a.n, GL_FLOAT, GL_FALSE, stride, reinterpret_cast<const void*>(a.off));
}
if (!kmdl.skin.empty()) {
glx::GenBuffers(1, &m.skin);
glx::BindBuffer(GL_ARRAY_BUFFER, m.skin);
glx::BufferData(GL_ARRAY_BUFFER, GLsizeiptr(kmdl.skin.size() * sizeof(KmdlSkin)), kmdl.skin.data(), GL_STATIC_DRAW);
glx::EnableVertexAttribArray(4);
glx::VertexAttribIPointer(4, 4, GL_UNSIGNED_BYTE, sizeof(KmdlSkin), reinterpret_cast<const void*>(offsetof(KmdlSkin, joints)));
glx::EnableVertexAttribArray(5);
glx::VertexAttribPointer(5, 4, GL_UNSIGNED_BYTE, GL_TRUE, sizeof(KmdlSkin), reinterpret_cast<const void*>(offsetof(KmdlSkin, weights)));
}
glx::GenBuffers(1, &m.indices);
glx::BindBuffer(GL_ELEMENT_ARRAY_BUFFER, m.indices);
glx::BufferData(GL_ELEMENT_ARRAY_BUFFER, GLsizeiptr(kmdl.indices.size() * 4), kmdl.indices.data(), GL_STATIC_DRAW);
glx::BindVertexArray(0);
m.skinned = !kmdl.skin.empty();
}
uint64_t uiTexture(TextureId slot) override { return slot < textures_.size() ? uint64_t(textures_[slot]) : 0; }
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;
ensureTargets(uint32_t(w), uint32_t(h));
width = uint32_t(w);
height = uint32_t(h);
return true;
}
bool takeHzb(HzbReadback&) override { return false; } // no occlusion culling here
void render(FramePacket& p) override;
private:
struct Model {
GLuint vao = 0, vertices = 0, indices = 0, skin = 0;
bool skinned = false;
};
Program link(const std::string& vs, const std::string& fs) {
auto compile = [](GLenum type, const std::string& src) -> GLuint {
const GLuint s = glx::CreateShader(type);
const char* text = src.c_str();
glx::ShaderSource(s, 1, &text, nullptr);
glx::CompileShader(s);
GLint ok = 0;
glx::GetShaderiv(s, GL_COMPILE_STATUS, &ok);
if (!ok) {
char logText[4096];
glx::GetShaderInfoLog(s, sizeof logText, nullptr, logText);
log::error("OpenGL (legacy): shader failed to compile:\n{}", logText);
glx::DeleteShader(s);
return 0;
}
return s;
};
Program p;
const GLuint v = compile(GL_VERTEX_SHADER, vs), f = compile(GL_FRAGMENT_SHADER, fs);
if (!v || !f) return p;
p.id = glx::CreateProgram();
glx::AttachShader(p.id, v);
glx::AttachShader(p.id, f);
const char* attrs[] = {"a_pos", "a_normal", "a_uv", "a_lmuv", "a_joints", "a_weights"};
for (GLuint i = 0; i < 6; ++i) glx::BindAttribLocation(p.id, i, attrs[i]);
glx::BindFragDataLocation(p.id, 0, "o_color");
glx::LinkProgram(p.id);
glx::DeleteShader(v);
glx::DeleteShader(f);
GLint ok = 0;
glx::GetProgramiv(p.id, GL_LINK_STATUS, &ok);
if (!ok) {
char logText[4096];
glx::GetProgramInfoLog(p.id, sizeof logText, nullptr, logText);
log::error("OpenGL (legacy): program failed to link:\n{}", logText);
glx::DeleteProgram(p.id);
p.id = 0;
return p;
}
if (const GLuint block = glx::GetUniformBlockIndex(p.id, "Frame"); block != GL_INVALID_INDEX) glx::UniformBlockBinding(p.id, block, 0);
auto u = [&](const char* n) { return glx::GetUniformLocation(p.id, n); };
p.viewProj = u("u_viewProj"), p.model = u("u_model"), p.skinned = u("u_skinned"), p.firstJoint = u("u_firstJoint");
p.tint = u("u_tint"), p.params = u("u_params"), p.plane = u("u_plane"), p.flags = u("u_flags"), p.cube = u("u_cube");
p.invViewProj = u("u_invViewProj"), p.center = u("u_center"), p.extents = u("u_extents"), p.color = u("u_color"), p.exposure = u("u_exposure");
// Fixed texture units.
glx::UseProgram(p.id);
const struct { const char* name; GLint unit; } units[] = {{"u_albedo", 0}, {"u_lightmap", 1}, {"u_material", 2}, {"u_skyVis", 3},
{"u_sunShadow", 4}, {"u_sky", 5}, {"u_joints", 6}, {"u_scene", 0}};
for (const auto& t : units)
if (const GLint loc = u(t.name); loc >= 0) glx::Uniform1i(loc, t.unit);
glx::UseProgram(0);
return p;
}
void releaseModel(Model& m) {
if (m.vao) glx::DeleteVertexArrays(1, &m.vao);
for (GLuint* b : {&m.vertices, &m.indices, &m.skin})
if (*b) glx::DeleteBuffers(1, b);
m = {};
}
void releaseTargets() {
if (sceneTex_) glx::DeleteTextures(1, &sceneTex_);
if (sceneDepth_) glx::DeleteTextures(1, &sceneDepth_);
if (sceneFbo_) glx::DeleteFramebuffers(1, &sceneFbo_);
sceneTex_ = sceneDepth_ = sceneFbo_ = 0;
}
void ensureTargets(uint32_t w, uint32_t h) {
if (sceneFbo_ && w == targetW_ && h == targetH_) return;
releaseTargets();
glx::GenTextures(1, &sceneTex_);
glx::BindTexture(GL_TEXTURE_2D, sceneTex_);
glx::TexImage2D(GL_TEXTURE_2D, 0, GL_RGBA16F, GLsizei(w), GLsizei(h), 0, GL_RGBA, GL_FLOAT, nullptr);
glx::TexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
glx::TexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
glx::TexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
glx::TexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);
glx::GenTextures(1, &sceneDepth_);
glx::BindTexture(GL_TEXTURE_2D, sceneDepth_);
glx::TexImage2D(GL_TEXTURE_2D, 0, GL_DEPTH_COMPONENT24, GLsizei(w), GLsizei(h), 0, GL_DEPTH_COMPONENT, GL_UNSIGNED_INT, nullptr);
glx::TexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
glx::TexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
glx::GenFramebuffers(1, &sceneFbo_);
glx::BindFramebuffer(GL_FRAMEBUFFER, sceneFbo_);
glx::FramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, sceneTex_, 0);
glx::FramebufferTexture2D(GL_FRAMEBUFFER, GL_DEPTH_ATTACHMENT, GL_TEXTURE_2D, sceneDepth_, 0);
if (glx::CheckFramebufferStatus(GL_FRAMEBUFFER) != GL_FRAMEBUFFER_COMPLETE) log::error("OpenGL (legacy): scene framebuffer incomplete");
glx::BindFramebuffer(GL_FRAMEBUFFER, 0);
targetW_ = w, targetH_ = h;
}
void bindTexture(GLint unit, TextureId slot) {
glx::ActiveTexture(GL_TEXTURE0 + GLenum(unit));
glx::BindTexture(GL_TEXTURE_2D, slot < textures_.size() && textures_[slot] ? textures_[slot] : textures_[kWhiteTexture]);
}
void drawMeshes(const FramePacket& p, const std::vector<MeshDraw>& draws, const glm::mat4& viewProj, const Program& prog, bool shadow);
std::string name_;
int major_, minor_, tier_;
SDL_Window* window_ = nullptr;
SDL_GLContext context_ = nullptr;
bool imgui_ = false;
Program mesh_, depth_, sky_, box_, tonemap_;
std::vector<GLuint> textures_;
std::vector<Model> models_;
GLuint frameUbo_ = 0, jointBuffer_ = 0, jointTexture_ = 0, emptyVao_ = 0;
GLuint shadowMap_ = 0, shadowFbo_ = 0;
uint32_t shadowSize_ = kShadowSize;
GLuint sceneTex_ = 0, sceneDepth_ = 0, sceneFbo_ = 0;
uint32_t targetW_ = 0, targetH_ = 0;
};
void LegacyGLBackend::drawMeshes(const FramePacket& p, const std::vector<MeshDraw>& draws, const glm::mat4& viewProj, const Program& prog, bool shadow) {
glx::UseProgram(prog.id);
glx::UniformMatrix4fv(prog.viewProj, 1, GL_FALSE, glm::value_ptr(viewProj));
uint32_t boundModel = UINT32_MAX;
for (const MeshDraw& d : draws) {
if (d.model >= models_.size() || !models_[d.model].vao) continue;
const Model& m = models_[d.model];
if (d.model != boundModel) glx::BindVertexArray(m.vao), boundModel = d.model;
const gpu::DrawData& dd = p.draws[d.draw];
const bool skinned = d.firstJoint != UINT32_MAX && m.skinned;
if (!skinned) glx::VertexAttribI4ui(4, 0, 0, 0, 0), glx::VertexAttrib4f(5, 1, 0, 0, 0);
glx::UniformMatrix4fv(prog.model, 1, GL_FALSE, glm::value_ptr(dd.model));
glx::Uniform1i(prog.skinned, skinned ? 1 : 0);
glx::Uniform1i(prog.firstJoint, skinned ? GLint(d.firstJoint) : 0);
bindTexture(0, d.textures & 0xFFFFu);
if (!shadow) {
const TextureId lightmap = d.textures >> 16, material = d.material & 0xFFFFu, skyVis = d.material >> 16;
if (lightmap) bindTexture(1, lightmap);
if (material && tier_ >= 2) bindTexture(2, material);
if (skyVis) bindTexture(3, skyVis);
const glm::vec4 tint = glm::unpackUnorm4x8(d.tint);
glx::Uniform4fv(prog.tint, 1, glm::value_ptr(tint));
glx::Uniform4fv(prog.params, 1, glm::value_ptr(dd.params));
glx::Uniform4fv(prog.plane, 1, glm::value_ptr(dd.plane));
const glm::vec4 flags(lightmap ? 1.0f : 0.0f, material ? 1.0f : 0.0f, skyVis ? 1.0f : 0.0f, 0.0f);
glx::Uniform4fv(prog.flags, 1, glm::value_ptr(flags));
glx::Uniform4fv(prog.cube, 6, glm::value_ptr(dd.cube[0]));
}
glx::DrawElements(GL_TRIANGLES, GLsizei(d.indexCount), GL_UNSIGNED_INT, reinterpret_cast<const void*>(uintptr_t(d.firstIndex) * 4));
}
glx::BindVertexArray(0);
}
void LegacyGLBackend::render(FramePacket& p) {
// ── Per-frame data: the Frame block (the 16 nearest lights on tier 2), joints ──
FrameBlock fb{};
std::memcpy(fb.cascades, p.frame.cascades, sizeof fb.cascades);
fb.splits = p.frame.splits, fb.cascadeTexel = p.frame.cascadeTexel, fb.sunDir = p.frame.sunDir, fb.sunColor = p.frame.sunColor;
fb.shadow = glm::vec4(1.0f / float(shadowSize_), p.frame.shadow.y, p.frame.shadow.z, 0);
fb.camPos = p.frame.camPos, fb.sky = p.frame.sky, fb.fog = p.frame.fog;
std::vector<const gpu::Light*> closest;
if (tier_ >= 2) {
for (const gpu::Light& l : p.lights) closest.push_back(&l);
const glm::vec3 cam(p.frame.camPos);
std::sort(closest.begin(), closest.end(), [&](auto* a, auto* b) {
return glm::length(glm::vec3(a->posRadius) - cam) - a->posRadius.w < glm::length(glm::vec3(b->posRadius) - cam) - b->posRadius.w;
});
closest.resize(std::min<size_t>(closest.size(), kMaxLegacyLights));
}
fb.lightCount = glm::vec4(float(closest.size()), glm::vec3(p.frame.camFwd));
for (size_t i = 0; i < closest.size(); ++i)
fb.lights[i * 4] = closest[i]->posRadius, fb.lights[i * 4 + 1] = closest[i]->color, fb.lights[i * 4 + 2] = closest[i]->spot, fb.lights[i * 4 + 3] = closest[i]->extra;
glx::BindBuffer(GL_UNIFORM_BUFFER, frameUbo_);
glx::BufferSubData(GL_UNIFORM_BUFFER, 0, sizeof fb, &fb);
glx::BindBufferBase(GL_UNIFORM_BUFFER, 0, frameUbo_);
if (!p.joints.empty()) {
glx::BindBuffer(GL_TEXTURE_BUFFER, jointBuffer_);
glx::BufferSubData(GL_TEXTURE_BUFFER, 0, GLsizeiptr(std::min(p.joints.size(), size_t(kMaxJointsPerFrame)) * sizeof(glm::mat4)), p.joints.data());
}
glx::ActiveTexture(GL_TEXTURE6);
glx::BindTexture(GL_TEXTURE_BUFFER, jointTexture_);
glx::Disable(GL_CULL_FACE);
glx::Disable(GL_BLEND);
glx::Enable(GL_DEPTH_TEST);
glx::DepthFunc(GL_LESS);
glx::DepthMask(GL_TRUE);
// ── Sun shadows (light shadows aren't drawn on this renderer) ──
glx::BindFramebuffer(GL_FRAMEBUFFER, shadowFbo_);
glx::Viewport(0, 0, GLsizei(shadowSize_), GLsizei(shadowSize_));
glx::ClearDepth(1.0);
glx::Clear(GL_DEPTH_BUFFER_BIT);
glx::Enable(GL_POLYGON_OFFSET_FILL);
glx::PolygonOffset(2.5f, 2.0f);
const float scale = float(shadowSize_) / float(p.shadowSize);
for (const ShadowPass& pass : p.sunPasses) {
glx::Viewport(GLint(pass.rect.x * scale), GLint(pass.rect.y * scale), GLsizei(pass.rect.z * scale), GLsizei(pass.rect.w * scale));
drawMeshes(p, *pass.draws, pass.viewProj, depth_, true);
}
glx::Disable(GL_POLYGON_OFFSET_FILL);
// ── Scene (HDR) ──
glx::BindFramebuffer(GL_FRAMEBUFFER, sceneFbo_);
glx::Viewport(0, 0, GLsizei(p.width), GLsizei(p.height));
glx::ClearColor(p.clearColor.r, p.clearColor.g, p.clearColor.b, 1.0f);
glx::Clear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
glx::ActiveTexture(GL_TEXTURE4);
glx::BindTexture(GL_TEXTURE_2D, shadowMap_);
const TextureId skyTex = TextureId(p.frame.sky.x);
if (skyTex) bindTexture(5, skyTex);
if (!p.boxes.empty()) {
glx::UseProgram(box_.id);
glx::UniformMatrix4fv(box_.viewProj, 1, GL_FALSE, glm::value_ptr(p.viewProj));
glx::BindVertexArray(emptyVao_);
for (const gpu::Box& b : p.boxes) {
glx::Uniform4fv(box_.center, 1, glm::value_ptr(b.center));
glx::Uniform4fv(box_.extents, 1, glm::value_ptr(b.extents));
glx::Uniform4fv(box_.color, 1, glm::value_ptr(b.color));
glx::DrawArrays(GL_TRIANGLES, 0, 36);
}
}
drawMeshes(p, p.opaque, p.viewProj, mesh_, false);
if (skyTex) {
glx::UseProgram(sky_.id);
glx::UniformMatrix4fv(sky_.invViewProj, 1, GL_FALSE, glm::value_ptr(p.invViewProj));
glx::DepthFunc(GL_LEQUAL);
glx::DepthMask(GL_FALSE);
glx::BindVertexArray(emptyVao_);
glx::DrawArrays(GL_TRIANGLES, 0, 3);
glx::DepthMask(GL_TRUE);
glx::DepthFunc(GL_LESS);
}
if (!p.translucent.empty()) {
glx::Enable(GL_BLEND);
glx::BlendFuncSeparate(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA, GL_ONE, GL_ONE_MINUS_SRC_ALPHA);
glx::DepthMask(GL_FALSE);
drawMeshes(p, p.translucent, p.viewProj, mesh_, false);
glx::DepthMask(GL_TRUE);
glx::Disable(GL_BLEND);
}
// ── Tonemap to the window, then UI ──
glx::BindFramebuffer(GL_FRAMEBUFFER, 0);
glx::Viewport(0, 0, GLsizei(p.width), GLsizei(p.height));
glx::Disable(GL_DEPTH_TEST);
glx::UseProgram(tonemap_.id);
const glm::vec4 exposure(p.exposure, 0, 0, 0);
glx::Uniform4fv(tonemap_.exposure, 1, glm::value_ptr(exposure));
glx::ActiveTexture(GL_TEXTURE0);
glx::BindTexture(GL_TEXTURE_2D, sceneTex_);
glx::BindVertexArray(emptyVao_);
glx::DrawArrays(GL_TRIANGLES, 0, 3);
glx::BindVertexArray(0);
glx::UseProgram(0);
ImGui_ImplOpenGL3_RenderDrawData(ImGui::GetDrawData());
if (!p.screenshot.empty()) {
std::vector<uint8_t> rows(size_t(p.width) * p.height * 4), px(rows.size());
glx::ReadBuffer(GL_BACK);
glx::ReadPixels(0, 0, GLsizei(p.width), GLsizei(p.height), GL_RGBA, GL_UNSIGNED_BYTE, rows.data());
const size_t stride = size_t(p.width) * 4; // the window's row 0 is its bottom
for (uint32_t y = 0; y < p.height; ++y) std::memcpy(&px[y * stride], &rows[(p.height - 1 - y) * stride], stride);
saveScreenshot(p.screenshot, std::move(px), p.width, p.height);
}
SDL_GL_SwapWindow(window_);
}
} // namespace
std::unique_ptr<RenderBackend> createLegacyOpenGLBackend(int major, int minor) { return std::make_unique<LegacyGLBackend>(major, minor); }
} // namespace keel

View file

@ -0,0 +1,21 @@
#include "rhi/gl/gl_loader.h"
#include "core/log.h"
#include <SDL3/SDL_video.h>
#include <string>
namespace keel::gl {
bool load() {
std::string missing;
#define KEEL_GL_LOAD(ret, name, params) \
name = reinterpret_cast<name##_t>(SDL_GL_GetProcAddress("gl" #name)); \
if (!name) missing += " gl" #name;
KEEL_GL_FUNCTIONS(KEEL_GL_LOAD)
#undef KEEL_GL_LOAD
if (!missing.empty()) log::warn("OpenGL: missing functions:{}", missing);
return missing.empty();
}
} // namespace keel::gl

87
engine/rhi/gl/gl_loader.h Normal file
View file

@ -0,0 +1,87 @@
#pragma once
// OpenGL 4.6 entry points, loaded through SDL at runtime (no GL library linked, no extra
// dependency): gl::Enable(...), gl::DrawElements(...). Types and constants come from SDL's
// copy of the Khronos headers. Add a function to KEEL_GL_FUNCTIONS to use it.
#include <SDL3/SDL_opengl.h>
#define KEEL_GL_FUNCTIONS(X) \
X(const GLubyte*, GetString, (GLenum name)) \
X(const GLubyte*, GetStringi, (GLenum name, GLuint index)) \
X(void, GetIntegerv, (GLenum pname, GLint* data)) \
X(GLenum, GetError, (void)) \
X(void, Enable, (GLenum cap)) \
X(void, Disable, (GLenum cap)) \
X(void, ClipControl, (GLenum origin, GLenum depth)) \
X(void, FrontFace, (GLenum mode)) \
X(void, Viewport, (GLint x, GLint y, GLsizei w, GLsizei h)) \
X(void, Scissor, (GLint x, GLint y, GLsizei w, GLsizei h)) \
X(void, DepthFunc, (GLenum func)) \
X(void, DepthMask, (GLboolean flag)) \
X(void, BlendFuncSeparate, (GLenum sc, GLenum dc, GLenum sa, GLenum da)) \
X(void, PolygonOffset, (GLfloat factor, GLfloat units)) \
X(void, PixelStorei, (GLenum pname, GLint param)) \
X(void, CreateBuffers, (GLsizei n, GLuint* buffers)) \
X(void, DeleteBuffers, (GLsizei n, const GLuint* buffers)) \
X(void, NamedBufferStorage, (GLuint buffer, GLsizeiptr size, const void* data, GLbitfield flags)) \
X(void, NamedBufferSubData, (GLuint buffer, GLintptr offset, GLsizeiptr size, const void* data)) \
X(void, BindBufferBase, (GLenum target, GLuint index, GLuint buffer)) \
X(void, CreateTextures, (GLenum target, GLsizei n, GLuint* textures)) \
X(void, DeleteTextures, (GLsizei n, const GLuint* textures)) \
X(void, TextureStorage2D, (GLuint texture, GLsizei levels, GLenum format, GLsizei w, GLsizei h)) \
X(void, TextureSubImage2D, (GLuint texture, GLint level, GLint x, GLint y, GLsizei w, GLsizei h, GLenum format, \
GLenum type, const void* pixels)) \
X(void, CompressedTextureSubImage2D, (GLuint texture, GLint level, GLint x, GLint y, GLsizei w, GLsizei h, \
GLenum format, GLsizei size, const void* data)) \
X(void, CreateSamplers, (GLsizei n, GLuint* samplers)) \
X(void, DeleteSamplers, (GLsizei n, const GLuint* samplers)) \
X(void, SamplerParameteri, (GLuint sampler, GLenum pname, GLint param)) \
X(void, SamplerParameterf, (GLuint sampler, GLenum pname, GLfloat param)) \
X(GLuint64, GetTextureSamplerHandleARB, (GLuint texture, GLuint sampler)) \
X(void, MakeTextureHandleResidentARB, (GLuint64 handle)) \
X(void, MakeTextureHandleNonResidentARB, (GLuint64 handle)) \
X(void, CreateFramebuffers, (GLsizei n, GLuint* framebuffers)) \
X(void, DeleteFramebuffers, (GLsizei n, const GLuint* framebuffers)) \
X(void, NamedFramebufferTexture, (GLuint fb, GLenum attachment, GLuint texture, GLint level)) \
X(void, NamedFramebufferDrawBuffer, (GLuint fb, GLenum buf)) \
X(GLenum, CheckNamedFramebufferStatus, (GLuint fb, GLenum target)) \
X(void, BindFramebuffer, (GLenum target, GLuint fb)) \
X(void, NamedFramebufferDrawBuffers, (GLuint fb, GLsizei n, const GLenum* bufs)) \
X(void, ColorMaski, (GLuint index, GLboolean r, GLboolean g, GLboolean b, GLboolean a)) \
X(void, ClearNamedFramebufferfv, (GLuint fb, GLenum buffer, GLint drawbuffer, const GLfloat* value)) \
X(GLuint, CreateShader, (GLenum type)) \
X(void, ShaderSource, (GLuint shader, GLsizei count, const GLchar* const* string, const GLint* length)) \
X(void, CompileShader, (GLuint shader)) \
X(void, GetShaderiv, (GLuint shader, GLenum pname, GLint* params)) \
X(void, GetShaderInfoLog, (GLuint shader, GLsizei max, GLsizei* length, GLchar* log)) \
X(void, DeleteShader, (GLuint shader)) \
X(GLuint, CreateProgram, (void)) \
X(void, AttachShader, (GLuint program, GLuint shader)) \
X(void, LinkProgram, (GLuint program)) \
X(void, GetProgramiv, (GLuint program, GLenum pname, GLint* params)) \
X(void, GetProgramInfoLog, (GLuint program, GLsizei max, GLsizei* length, GLchar* log)) \
X(void, DeleteProgram, (GLuint program)) \
X(void, UseProgram, (GLuint program)) \
X(void, CreateVertexArrays, (GLsizei n, GLuint* arrays)) \
X(void, DeleteVertexArrays, (GLsizei n, const GLuint* arrays)) \
X(void, BindVertexArray, (GLuint array)) \
X(void, VertexArrayElementBuffer, (GLuint vao, GLuint buffer)) \
X(void, DrawElements, (GLenum mode, GLsizei count, GLenum type, const void* indices)) \
X(void, DrawArrays, (GLenum mode, GLint first, GLsizei count)) \
X(void, ReadPixels, (GLint x, GLint y, GLsizei w, GLsizei h, GLenum format, GLenum type, void* pixels)) \
X(void, ReadBuffer, (GLenum src)) \
X(GLsync, FenceSync, (GLenum condition, GLbitfield flags)) \
X(GLenum, ClientWaitSync, (GLsync sync, GLbitfield flags, GLuint64 timeout)) \
X(void, DeleteSync, (GLsync sync)) \
X(void, BindBuffer, (GLenum target, GLuint buffer)) \
X(void*, MapNamedBufferRange, (GLuint buffer, GLintptr offset, GLsizeiptr length, GLbitfield access)) \
X(GLboolean, UnmapNamedBuffer, (GLuint buffer)) \
X(void, DebugMessageCallback, (GLDEBUGPROC callback, const void* user))
namespace keel::gl {
#define KEEL_GL_DECLARE(ret, name, params) using name##_t = ret(APIENTRY*) params; inline name##_t name = nullptr;
KEEL_GL_FUNCTIONS(KEEL_GL_DECLARE)
#undef KEEL_GL_DECLARE
// Loads every entry point from the current context; false (and logs which) if any is missing.
bool load();
} // namespace keel::gl