mirror of
https://github.com/Nexoniarz/Fry3DX.git
synced 2026-11-03 04:24:26 +00:00
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:
commit
f3e8eca87b
135 changed files with 23423 additions and 0 deletions
858
engine/rhi/gl/gl_backend.cpp
Normal file
858
engine/rhi/gl/gl_backend.cpp
Normal 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
820
engine/rhi/gl/gl_legacy.cpp
Normal 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
|
||||
21
engine/rhi/gl/gl_loader.cpp
Normal file
21
engine/rhi/gl/gl_loader.cpp
Normal 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
87
engine/rhi/gl/gl_loader.h
Normal 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
|
||||
Loading…
Reference in a new issue