Framebuffers and their attachments, renderbuffers (multisampled too), draw and read buffers, completeness checks, blits, viewports, clears, the screen framebuffer, the multisample enable, the wireframe switch and GL error checks now go through gpu_fb_* / gpu_rb_* / gpu_viewport / gpu_clear / gpu_blit / gpu_check, and no other file names them. Each call is the one GL call it replaces, in the same order: the fixed viewpoints render bit-identically and the game's self-tests report exactly what they did. What is attached to each framebuffer - colour slots, a depth texture or one layer of an array, renderbuffers and their samples - is recorded as it is attached, for a backend that builds render passes and image views. gpu_read_screen is the frame read-back a photograph takes. R3D_GLCHECK=1 checks, around every draw, clear and blit, that the bound framebuffer is complete and that no error is left behind, naming the framebuffer. It has already narrowed the old "gl error 1286 at terrain shadow bake": the error is pending before a draw after the map self- test, so it comes from a call that is not a draw, clear or blit. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
126 lines
5.6 KiB
Text
126 lines
5.6 KiB
Text
# ============================================================================
|
|
# programs.ludic — GLSL from files. A shader file carries no #version line; the
|
|
# loader prepends "#version 410 core", any per-variant defines, and the shared
|
|
# noise.glsl + lighting.glsl chunks for fragment stages, then compiles it.
|
|
# ============================================================================
|
|
|
|
# Where the renderer's own files are. The shaders belong to this package and ship with
|
|
# it — in the Ludic checkout they are under packages/, and in an installed toolchain
|
|
# under $LUDIC_HOME/packages/ — so a game built outside the Ludic tree does not have to
|
|
# copy them in. The scanned CC0 materials are the other half: too large to ship with a
|
|
# toolchain and redistributable from their origin, so they are fetched into the project
|
|
# (`ludic assets`) and read from there.
|
|
var r3d_root: string = "packages/ludic.render3d" # where shaders/ lives
|
|
var r3d_assets: string = "assets/polyhaven" # where the CC0 assets live
|
|
var r3d_root_found: bool = false
|
|
# defines prepended to EVERY program (set before any is built): renderer-wide switches
|
|
var r3d_global_defs: string = ""
|
|
var r3d_noise_src: string = null
|
|
var r3d_lighting_src: string = null
|
|
|
|
function r3d_set_paths(root: string, assets: string) -> void { r3d_root = root; r3d_assets = assets }
|
|
|
|
# The install root, as the compiler computes it: $LUDIC_HOME, else the directory of the
|
|
# `ludic` on PATH. A game running from its own tree finds this package there.
|
|
function r3d_home() -> string {
|
|
let env = Os.env("LUDIC_HOME")
|
|
if env != null and env != "" { return env }
|
|
return `{Os.env("HOME")}/.ludic`
|
|
}
|
|
|
|
# Settle r3d_root on first use: the package as checked out beside the project, else the
|
|
# copy that ships with the toolchain.
|
|
function r3d_find_root() -> void {
|
|
if r3d_root_found { return }
|
|
r3d_root_found = true
|
|
if Fs.exists(`{r3d_root}/shaders/lighting.glsl`) { return }
|
|
let home = r3d_home()
|
|
let alt = `{home}/packages/ludic.render3d`
|
|
if Fs.exists(`{alt}/shaders/lighting.glsl`) { r3d_root = alt; return }
|
|
print(`r3d: cannot find the renderer's shaders (looked in {r3d_root}/shaders and {alt}/shaders)`)
|
|
}
|
|
|
|
function r3d_shader_file(name: string) -> string {
|
|
r3d_find_root()
|
|
let path = `{r3d_root}/shaders/{name}`
|
|
let s = Fs.read_text(path)
|
|
if s == null { print(`r3d: missing shader {path}`); return "" }
|
|
return s
|
|
}
|
|
|
|
function r3d_shader_src(name: string, defines: string, is_frag: bool) -> string {
|
|
if r3d_noise_src == null { r3d_noise_src = r3d_shader_file("noise.glsl") }
|
|
if r3d_lighting_src == null { r3d_lighting_src = r3d_shader_file("lighting.glsl") }
|
|
var s = "#version 410 core\n" + r3d_global_defs + defines
|
|
if is_frag { s = s + r3d_noise_src + r3d_lighting_src }
|
|
return s + r3d_shader_file(name)
|
|
}
|
|
|
|
# Build a program with tessellation control/evaluation between vertex and fragment.
|
|
function r3d_program_tess(vs: string, tcs: string, tes: string, fs: string, defines: string) -> int {
|
|
let p = gl_program5(r3d_shader_src(vs, defines, false), r3d_shader_src(tcs, defines, false),
|
|
r3d_shader_src(tes, defines, false), null, r3d_shader_src(fs, defines, true))
|
|
if p == 0 { print(`r3d: tess program failed: {vs} + {tcs} + {tes} + {fs}`) }
|
|
return p
|
|
}
|
|
|
|
# Build a program from a vertex + fragment file pair (defines apply to both).
|
|
# R3D_PROGRAMS_LOG=<file>: append every program built (vertex, fragment, defines) - the list a
|
|
# backend that cannot compile shaders at run time (Vulkan: SPIR-V is built ahead) must cover
|
|
var r3d_prog_log: int = -1
|
|
function r3d_program_log(vs: string, fs: string, defines: string) -> void {
|
|
if r3d_prog_log < 0 { r3d_prog_log = 0; if Os.has_env("R3D_PROGRAMS_LOG") { r3d_prog_log = 1 } }
|
|
if r3d_prog_log == 0 { return }
|
|
let f = file_open(Os.env("R3D_PROGRAMS_LOG"), "ab")
|
|
if f == null { return }
|
|
# one line per program: the defines' newlines become ';' so a variant is one line
|
|
let defs = Text.replace(`{r3d_global_defs}{defines}`, "\n", ";")
|
|
let line = `{vs}|{fs}|{defs}\n`
|
|
file_write(f, line, len(line))
|
|
file_close(f)
|
|
}
|
|
function r3d_program(vs: string, fs: string, defines: string) -> int {
|
|
r3d_program_log(vs, fs, defines)
|
|
let p = gl_program(r3d_shader_src(vs, defines, false), r3d_shader_src(fs, defines, true))
|
|
if p == 0 { print(`r3d: program failed: {vs} + {fs}`) }
|
|
return p
|
|
}
|
|
|
|
# Bind a texture to a unit and point a sampler uniform at it.
|
|
function r3d_bind_tex(prog: int, name: string, unit: int, kind: int, tex: int) -> void { gpu_bind_sampler(prog, name, unit, kind, tex) }
|
|
function r3d_bind_2d(prog: int, name: string, unit: int, tex: int) -> void { gpu_bind_sampler(prog, name, unit, GPU_TEX2D, tex) }
|
|
|
|
# A framebuffer with one colour texture (and optionally a depth texture).
|
|
property Target {
|
|
fbo: int = 0,
|
|
color: int = 0,
|
|
depth: int = 0,
|
|
w: int = 0,
|
|
h: int = 0
|
|
}
|
|
function target_new(w: int, h: int, ifmt: int, fmt: int, ty: int, with_depth: bool, filter: int) -> Target {
|
|
let t = new Target
|
|
t.w = w; t.h = h
|
|
t.fbo = gpu_fb_new()
|
|
gpu_fb_bind(t.fbo)
|
|
t.color = tex_target(w, h, ifmt, fmt, ty, filter)
|
|
gpu_fb_color(0, t.color)
|
|
if with_depth {
|
|
t.depth = tex_target(w, h, GL_DEPTH_COMPONENT32F, GL_DEPTH_COMPONENT, GL_FLOAT, GL_NEAREST)
|
|
gpu_fb_depth(t.depth)
|
|
}
|
|
let st = gpu_fb_status()
|
|
if st != GL_FRAMEBUFFER_COMPLETE { print(`r3d: framebuffer incomplete {st} ({w}x{h})`) }
|
|
gpu_fb_bind(0)
|
|
return t
|
|
}
|
|
function target_free(t: Target) -> void {
|
|
if t == null { return }
|
|
gpu_fb_free(t.fbo)
|
|
gpu_tex_free(t.color)
|
|
if t.depth != 0 { gpu_tex_free(t.depth) }
|
|
}
|
|
function target_bind(t: Target) -> void {
|
|
gpu_fb_bind(t.fbo)
|
|
gpu_viewport(0, 0, t.w, t.h)
|
|
}
|