mirror of
https://github.com/Nexoniarz/Fry3DX.git
synced 2026-11-03 12:34:27 +00:00
875 lines
42 KiB
C++
875 lines
42 KiB
C++
// The OpenGL 4.6 backend. Same shaders and passes as Vulkan:
|
|
// - textures are ARB_bindless_texture handles in a storage-buffer table, indexed exactly like
|
|
// Vulkan's descriptor array; shadow maps also get comparison handles in a second table
|
|
// - per-draw / per-frame data are storage buffers at fixed bindings (model.glsl); the push
|
|
// constants become a small uniform block, rewritten per draw
|
|
// - glClipControl(LOWER_LEFT, ZERO_TO_ONE): with GL's own origin, clip-space y = -1 lands in
|
|
// memory row 0 exactly as in Vulkan (whose projection we share, y flipped), so images,
|
|
// shadow-map tiles and gl_FragCoord all match; depth is 0..1 like Vulkan. The y flip reverses
|
|
// window-space winding, so front faces are clockwise. Only the final blit to the window
|
|
// (which shows row 0 at the bottom) flips.
|
|
// Requires ARB_bindless_texture (NVIDIA, AMD); without it init fails and the engine uses Vulkan.
|
|
#include "ui/ui.h"
|
|
#include "render/backend.h"
|
|
#include "render/screenshot.h"
|
|
#include "rhi/gl/gl_loader.h"
|
|
#include "core/log.h"
|
|
#include "platform/window.h"
|
|
|
|
#include <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, &uiRawSampler_); // (UI pictures: sRGB read as plain numbers, as ImGui's colours are)
|
|
gl::SamplerParameteri(uiRawSampler_, GL_TEXTURE_MIN_FILTER, GL_LINEAR_MIPMAP_LINEAR);
|
|
gl::SamplerParameteri(uiRawSampler_, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
|
|
gl::SamplerParameteri(uiRawSampler_, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
|
|
gl::SamplerParameteri(uiRawSampler_, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);
|
|
gl::SamplerParameteri(uiRawSampler_, 0x8A48 /* GL_TEXTURE_SRGB_DECODE_EXT */, 0x8A4A /* GL_SKIP_DECODE_EXT */);
|
|
gl::CreateSamplers(1, &compareSampler_);
|
|
gl::SamplerParameteri(compareSampler_, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
|
|
gl::SamplerParameteri(compareSampler_, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
|
|
gl::SamplerParameteri(compareSampler_, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
|
|
gl::SamplerParameteri(compareSampler_, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);
|
|
gl::SamplerParameteri(compareSampler_, GL_TEXTURE_COMPARE_MODE, GL_COMPARE_REF_TO_TEXTURE);
|
|
gl::SamplerParameteri(compareSampler_, GL_TEXTURE_COMPARE_FUNC, GL_LEQUAL);
|
|
|
|
auto buffer = [](GLsizeiptr size) {
|
|
GLuint b;
|
|
gl::CreateBuffers(1, &b);
|
|
gl::NamedBufferStorage(b, size, nullptr, GL_DYNAMIC_STORAGE_BIT);
|
|
return b;
|
|
};
|
|
textureTable_ = buffer(kMaxTextures * sizeof(GLuint64));
|
|
shadowTable_ = buffer(kMaxTextures * sizeof(GLuint64));
|
|
draws_ = buffer(kMaxDraws * sizeof(gpu::DrawData));
|
|
joints_ = buffer(kMaxJointsPerFrame * sizeof(glm::mat4));
|
|
frame_ = buffer(sizeof(gpu::FrameData));
|
|
mirrorFrame_ = buffer(sizeof(gpu::FrameData));
|
|
lights_ = buffer(kMaxLights * sizeof(gpu::Light));
|
|
views_ = buffer(kMaxShadowViews * sizeof(gpu::ShadowView));
|
|
cells_ = buffer(kClusterX * kClusterY * kClusterZ * sizeof(glm::uvec2));
|
|
indices_ = buffer(kMaxLightIndices * sizeof(uint32_t));
|
|
push_ = buffer(256);
|
|
empty_ = buffer(16); // bound where a shader declares a buffer the draw doesn't use
|
|
gl::CreateVertexArrays(1, &vao_);
|
|
|
|
meshProgram_ = program("mesh.vert", "mesh.frag");
|
|
skinnedProgram_ = program("skinned.vert", "mesh.frag");
|
|
shadowMeshProgram_ = program("mesh.vert", "");
|
|
shadowSkinnedProgram_ = program("skinned.vert", "");
|
|
boxProgram_ = program("box.vert", "box.frag");
|
|
tonemapProgram_ = program("fullscreen.vert", "tonemap.frag");
|
|
skyProgram_ = program("sky.vert", "sky.frag");
|
|
hzbProgram_ = program("fullscreen.vert", "hzb.frag");
|
|
ssaoProgram_ = program("fullscreen.vert", "ssao.frag");
|
|
ssaoBlurProgram_ = program("fullscreen.vert", "ssao_blur.frag");
|
|
bloomDownProgram_ = program("fullscreen.vert", "bloom_down.frag");
|
|
bloomUpProgram_ = program("fullscreen.vert", "bloom_up.frag");
|
|
taaProgram_ = program("fullscreen.vert", "taa.frag");
|
|
ssrProgram_ = program("fullscreen.vert", "ssr.frag");
|
|
adaptProgram_ = program("fullscreen.vert", "adapt.frag");
|
|
volumetricProgram_ = program("fullscreen.vert", "volumetric.frag");
|
|
spriteProgram_ = program("sprite.vert", "sprite.frag");
|
|
gl::CreateTextures(GL_TEXTURE_2D, 1, &hzbTex_);
|
|
gl::TextureStorage2D(hzbTex_, 1, GL_R32F, GLsizei(kHzbWidth), GLsizei(kHzbHeight));
|
|
gl::CreateFramebuffers(1, &hzbFbo_);
|
|
gl::NamedFramebufferTexture(hzbFbo_, GL_COLOR_ATTACHMENT0, hzbTex_, 0);
|
|
gl::CreateBuffers(kHzbRing, hzbPbo_);
|
|
for (GLuint b : hzbPbo_) gl::NamedBufferStorage(b, kHzbWidth * kHzbHeight * 4, nullptr, GL_MAP_READ_BIT | GL_CLIENT_STORAGE_BIT);
|
|
if (!meshProgram_ || !skinnedProgram_ || !shadowMeshProgram_ || !shadowSkinnedProgram_ || !boxProgram_ || !tonemapProgram_ || !skyProgram_)
|
|
return false;
|
|
|
|
sunShadow_ = depthTarget(kShadowSize, sunShadowFbo_);
|
|
atlas_ = depthTarget(kAtlasSize, atlasFbo_);
|
|
setHandles(kSunShadowSlot, sunShadow_, clampSampler_, true);
|
|
setHandles(kAtlasSlot, atlas_, clampSampler_, true);
|
|
|
|
IMGUI_CHECKVERSION();
|
|
ImGui::CreateContext();
|
|
ImGui::GetIO().IniFilename = nullptr;
|
|
ImGui::StyleColorsDark();
|
|
imgui_ = ImGui_ImplSDL3_InitForOpenGL(window_, context_) && ImGui_ImplOpenGL3_Init("#version 460");
|
|
return imgui_;
|
|
}
|
|
|
|
void shutdown() override {
|
|
if (!context_) return;
|
|
if (imgui_) {
|
|
ImGui_ImplOpenGL3_Shutdown();
|
|
ImGui_ImplSDL3_Shutdown();
|
|
ImGui::DestroyContext();
|
|
imgui_ = false;
|
|
}
|
|
if (gl::DeleteBuffers) {
|
|
for (auto& [slot, t] : textures_) releaseTexture(t);
|
|
for (Model& m : models_) releaseModel(m);
|
|
releaseTargets();
|
|
for (GLuint* t : {&sunShadow_.texture, &atlas_.texture}) gl::DeleteTextures(1, t);
|
|
for (GLuint* f : {&sunShadowFbo_, &atlasFbo_, &hzbFbo_}) gl::DeleteFramebuffers(1, f);
|
|
gl::DeleteTextures(1, &hzbTex_);
|
|
for (int i = 0; i < kHzbRing; ++i) {
|
|
if (hzbFence_[i]) gl::DeleteSync(hzbFence_[i]);
|
|
gl::DeleteBuffers(1, &hzbPbo_[i]);
|
|
}
|
|
for (GLuint b : {textureTable_, shadowTable_, draws_, joints_, frame_, mirrorFrame_, lights_, views_, cells_, indices_, push_, empty_})
|
|
gl::DeleteBuffers(1, &b);
|
|
for (GLuint s : {repeatSampler_, clampSampler_, compareSampler_, uiRawSampler_}) gl::DeleteSamplers(1, &s);
|
|
for (GLuint p : {meshProgram_, skinnedProgram_, shadowMeshProgram_, shadowSkinnedProgram_, boxProgram_, tonemapProgram_, skyProgram_, hzbProgram_, ssaoProgram_, ssaoBlurProgram_, ssrProgram_, adaptProgram_, volumetricProgram_, spriteProgram_,
|
|
bloomDownProgram_, bloomUpProgram_, taaProgram_})
|
|
gl::DeleteProgram(p);
|
|
gl::DeleteVertexArrays(1, &vao_);
|
|
}
|
|
SDL_GL_DestroyContext(context_);
|
|
context_ = nullptr;
|
|
}
|
|
|
|
void beginUi() override {
|
|
ImGui_ImplOpenGL3_NewFrame();
|
|
ImGui_ImplSDL3_NewFrame();
|
|
ui::applyPixelScale();
|
|
ImGui::NewFrame();
|
|
}
|
|
void setVsync(bool on) override { SDL_GL_SetSwapInterval(on ? 1 : 0); }
|
|
void onResize() override {}
|
|
|
|
bool blockCompression() const override { return true; } // BPTC (BC7) is core since 4.2
|
|
bool renderScale() const override { return true; }
|
|
|
|
void createTexture(TextureId slot, uint32_t w, uint32_t h, bool srgb, const std::vector<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());
|
|
}
|
|
t.srgb = srgb;
|
|
if (auto it = textures_.find(slot); it != textures_.end()) retiredTextures_.emplace_back(frameNumber_, it->second); // queued draws may use it
|
|
setHandles(slot, t, repeatSampler_, false); // like Vulkan: one repeating sampler for every texture
|
|
textures_[slot] = t;
|
|
}
|
|
|
|
void uploadModel(uint32_t index, const KmdlModel& kmdl) override {
|
|
if (models_.size() <= index) models_.resize(index + 1);
|
|
releaseModel(models_[index]); // GL keeps it alive while queued draws still use it
|
|
Model m;
|
|
auto upload = [](const void* data, size_t size) {
|
|
GLuint b;
|
|
gl::CreateBuffers(1, &b);
|
|
gl::NamedBufferStorage(b, GLsizeiptr(size), data, 0);
|
|
return b;
|
|
};
|
|
if (!kmdl.vertices.empty() && !kmdl.indices.empty()) {
|
|
m.vertices = upload(kmdl.vertices.data(), kmdl.vertices.size() * sizeof(KmdlVertex));
|
|
m.indices = upload(kmdl.indices.data(), kmdl.indices.size() * sizeof(uint32_t));
|
|
if (!kmdl.skin.empty()) m.skin = upload(kmdl.skin.data(), kmdl.skin.size() * sizeof(KmdlSkin));
|
|
}
|
|
models_[index] = m;
|
|
}
|
|
|
|
uint64_t uiTexture(TextureId slot) override {
|
|
const auto it = textures_.find(slot);
|
|
return it == textures_.end() ? 0 : uint64_t(it->second.texture);
|
|
}
|
|
UiRawRead uiRawRead(TextureId slot) override {
|
|
const auto it = textures_.find(slot);
|
|
if (it == textures_.end() || !it->second.srgb) return {};
|
|
// (A sampler, not the texture's own setting: that's fixed once it has a bindless handle.)
|
|
return {[](const ImDrawList*, const ImDrawCmd* c) { gl::BindSampler(0, GLuint(uintptr_t(c->UserCallbackData))); },
|
|
[](const ImDrawList*, const ImDrawCmd*) { gl::BindSampler(0, 0); }, reinterpret_cast<void*>(uintptr_t(uiRawSampler_))};
|
|
}
|
|
|
|
bool beginFrame(uint32_t& width, uint32_t& height) override {
|
|
int w = 0, h = 0;
|
|
if (SDL_GetWindowFlags(window_) & SDL_WINDOW_MINIMIZED) return false;
|
|
SDL_GetWindowSizeInPixels(window_, &w, &h);
|
|
if (w <= 0 || h <= 0) return false;
|
|
width = uint32_t(w); // (targets are sized to the scene in render)
|
|
height = uint32_t(h);
|
|
return true;
|
|
}
|
|
|
|
void render(FramePacket& p) override;
|
|
|
|
// Reads ring slot i's grid if the GPU is done with it (or, wait: before reusing the slot).
|
|
void collectHzb(int i, bool wait) {
|
|
if (!hzbFence_[i]) return;
|
|
const GLenum r = gl::ClientWaitSync(hzbFence_[i], wait ? GL_SYNC_FLUSH_COMMANDS_BIT : 0, wait ? 1'000'000'000 : 0);
|
|
if (r != GL_ALREADY_SIGNALED && r != GL_CONDITION_SATISFIED) return;
|
|
gl::DeleteSync(hzbFence_[i]);
|
|
hzbFence_[i] = nullptr;
|
|
if (hzbTags_[i] <= hzbLatest_.tag) return;
|
|
if (const float* d = static_cast<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;
|
|
bool srgb = false;
|
|
};
|
|
struct Model {
|
|
GLuint vertices = 0, indices = 0, skin = 0;
|
|
};
|
|
|
|
void setHandles(TextureId slot, Texture& t, GLuint sampler, bool depth) {
|
|
t.handle = gl::GetTextureSamplerHandleARB(t.texture, sampler);
|
|
gl::MakeTextureHandleResidentARB(t.handle);
|
|
gl::NamedBufferSubData(textureTable_, GLintptr(slot) * 8, 8, &t.handle);
|
|
if (depth) {
|
|
t.shadowHandle = gl::GetTextureSamplerHandleARB(t.texture, compareSampler_);
|
|
gl::MakeTextureHandleResidentARB(t.shadowHandle);
|
|
gl::NamedBufferSubData(shadowTable_, GLintptr(slot) * 8, 8, &t.shadowHandle);
|
|
}
|
|
}
|
|
void releaseTexture(Texture& t) {
|
|
if (t.handle) gl::MakeTextureHandleNonResidentARB(t.handle);
|
|
if (t.shadowHandle) gl::MakeTextureHandleNonResidentARB(t.shadowHandle);
|
|
if (t.texture) gl::DeleteTextures(1, &t.texture);
|
|
t = {};
|
|
}
|
|
void releaseModel(Model& m) {
|
|
for (GLuint* b : {&m.vertices, &m.indices, &m.skin})
|
|
if (*b) gl::DeleteBuffers(1, b);
|
|
m = {};
|
|
}
|
|
Texture depthTarget(uint32_t size, GLuint& fbo) {
|
|
Texture t;
|
|
gl::CreateTextures(GL_TEXTURE_2D, 1, &t.texture);
|
|
gl::TextureStorage2D(t.texture, 1, GL_DEPTH_COMPONENT32F, GLsizei(size), GLsizei(size));
|
|
gl::CreateFramebuffers(1, &fbo);
|
|
gl::NamedFramebufferTexture(fbo, GL_DEPTH_ATTACHMENT, t.texture, 0);
|
|
gl::NamedFramebufferDrawBuffer(fbo, GL_NONE);
|
|
return t;
|
|
}
|
|
void releaseTargets() {
|
|
releaseTexture(scene_);
|
|
releaseTexture(gbuf_);
|
|
releaseTexture(mirror_);
|
|
releaseTexture(mirrorGbuf_);
|
|
if (mirrorDepth_) gl::DeleteTextures(1, &mirrorDepth_);
|
|
if (mirrorFbo_) gl::DeleteFramebuffers(1, &mirrorFbo_);
|
|
mirrorDepth_ = mirrorFbo_ = 0;
|
|
releasePostTarget(ssr_);
|
|
for (PostTarget& t : adapt_) releasePostTarget(t);
|
|
releasePostTarget(ao_);
|
|
releasePostTarget(aoBlur_);
|
|
for (PostTarget& t : bloom_) releasePostTarget(t);
|
|
for (PostTarget& t : taa_) releasePostTarget(t);
|
|
if (depthTex_.handle) gl::MakeTextureHandleNonResidentARB(depthTex_.handle);
|
|
depthTex_ = {};
|
|
if (sceneDepth_) gl::DeleteTextures(1, &sceneDepth_);
|
|
if (sceneFbo_) gl::DeleteFramebuffers(1, &sceneFbo_);
|
|
sceneDepth_ = sceneFbo_ = 0;
|
|
}
|
|
void ensureTargets(uint32_t w, uint32_t h) {
|
|
if (scene_.texture && w == targetW_ && h == targetH_) return;
|
|
releaseTargets();
|
|
gl::CreateTextures(GL_TEXTURE_2D, 1, &scene_.texture);
|
|
gl::TextureStorage2D(scene_.texture, 1, GL_RGBA16F, GLsizei(w), GLsizei(h));
|
|
gl::CreateTextures(GL_TEXTURE_2D, 1, &sceneDepth_);
|
|
gl::TextureStorage2D(sceneDepth_, 1, GL_DEPTH_COMPONENT32F, GLsizei(w), GLsizei(h));
|
|
gl::CreateFramebuffers(1, &sceneFbo_);
|
|
gl::NamedFramebufferTexture(sceneFbo_, GL_COLOR_ATTACHMENT0, scene_.texture, 0);
|
|
gl::NamedFramebufferTexture(sceneFbo_, GL_DEPTH_ATTACHMENT, sceneDepth_, 0);
|
|
gl::CreateTextures(GL_TEXTURE_2D, 1, &gbuf_.texture); // the G-buffer: view normal, roughness, reflection weight
|
|
gl::TextureStorage2D(gbuf_.texture, 1, GL_RGBA16F, GLsizei(w), GLsizei(h));
|
|
gl::NamedFramebufferTexture(sceneFbo_, GL_COLOR_ATTACHMENT1, gbuf_.texture, 0);
|
|
const GLenum bufs[2] = {GL_COLOR_ATTACHMENT0, GL_COLOR_ATTACHMENT1};
|
|
gl::NamedFramebufferDrawBuffers(sceneFbo_, 2, bufs);
|
|
setHandles(kGbufSlot, gbuf_, clampSampler_, false);
|
|
if (gl::CheckNamedFramebufferStatus(sceneFbo_, GL_FRAMEBUFFER) != GL_FRAMEBUFFER_COMPLETE) log::error("OpenGL: scene framebuffer incomplete");
|
|
setHandles(kSceneSlot, scene_, clampSampler_, false);
|
|
gl::CreateTextures(GL_TEXTURE_2D, 1, &mirror_.texture);
|
|
gl::TextureStorage2D(mirror_.texture, 1, GL_RGBA16F, GLsizei(w), GLsizei(h));
|
|
gl::CreateTextures(GL_TEXTURE_2D, 1, &mirrorGbuf_.texture);
|
|
gl::TextureStorage2D(mirrorGbuf_.texture, 1, GL_RGBA16F, GLsizei(w), GLsizei(h));
|
|
gl::CreateTextures(GL_TEXTURE_2D, 1, &mirrorDepth_);
|
|
gl::TextureStorage2D(mirrorDepth_, 1, GL_DEPTH_COMPONENT32F, GLsizei(w), GLsizei(h));
|
|
gl::CreateFramebuffers(1, &mirrorFbo_);
|
|
gl::NamedFramebufferTexture(mirrorFbo_, GL_COLOR_ATTACHMENT0, mirror_.texture, 0);
|
|
gl::NamedFramebufferTexture(mirrorFbo_, GL_COLOR_ATTACHMENT1, mirrorGbuf_.texture, 0);
|
|
gl::NamedFramebufferTexture(mirrorFbo_, GL_DEPTH_ATTACHMENT, mirrorDepth_, 0);
|
|
gl::NamedFramebufferDrawBuffers(mirrorFbo_, 2, bufs);
|
|
setHandles(kMirrorSlot, mirror_, clampSampler_, false);
|
|
depthTex_.texture = sceneDepth_;
|
|
setHandles(kDepthSlot, depthTex_, clampSampler_, false);
|
|
ao_ = makePostTarget(kAoSlot, GL_R8, std::max(1u, w / 2), std::max(1u, h / 2));
|
|
aoBlur_ = makePostTarget(kAoBlurSlot, GL_R8, std::max(1u, w / 2), std::max(1u, h / 2));
|
|
ssr_ = makePostTarget(kSsrSlot, GL_RGBA16F, std::max(1u, w / 2), std::max(1u, h / 2));
|
|
for (uint32_t i = 0; i < 2; ++i) adapt_[i] = makePostTarget(kAdaptSlot + i, GL_RGBA16F, 1, 1);
|
|
adaptValid_ = false;
|
|
for (uint32_t i = 0; i < 2; ++i) taa_[i] = makePostTarget(kTaaSlot + i, GL_RGBA16F, w, h);
|
|
for (uint32_t i = 0; i < kBloomLevels; ++i)
|
|
bloom_[i] = makePostTarget(kBloomSlot + i, GL_RGBA16F, std::max(1u, w >> (i + 1)), std::max(1u, h >> (i + 1)));
|
|
targetW_ = w;
|
|
targetH_ = h;
|
|
}
|
|
template <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, uiRawSampler_ = 0;
|
|
GLuint mirrorFrame_ = 0; // the mirror's pass's frame data
|
|
GLuint textureTable_ = 0, shadowTable_ = 0, draws_ = 0, joints_ = 0, frame_ = 0, lights_ = 0, views_ = 0, cells_ = 0, indices_ = 0;
|
|
GLuint push_ = 0, empty_ = 0, vao_ = 0;
|
|
GLuint meshProgram_ = 0, skinnedProgram_ = 0, shadowMeshProgram_ = 0, shadowSkinnedProgram_ = 0, boxProgram_ = 0, tonemapProgram_ = 0, skyProgram_ = 0, hzbProgram_ = 0;
|
|
GLuint ssaoProgram_ = 0, ssaoBlurProgram_ = 0, bloomDownProgram_ = 0, bloomUpProgram_ = 0, taaProgram_ = 0, ssrProgram_ = 0, adaptProgram_ = 0, volumetricProgram_ = 0, spriteProgram_ = 0;
|
|
uint32_t adaptCur_ = 0;
|
|
bool adaptValid_ = false;
|
|
uint32_t taaCur_ = 0;
|
|
uint64_t frameNumber_ = 0;
|
|
std::vector<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
|