feat(render3d): gate R3D_* switches behind R3D_DEV in windowed builds

Every R3D_* read goes through r3d_env_has / r3d_env (env.ludic): a headless
build honours them as before, a windowed build only when R3D_DEV is set to
anything but "0", so a shipped game never reaches a debug view, feature kill,
file writer or hardware fake. Documented in docs/SHIPPING.md.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
Orkun ÇAKILKAYA 2026-09-16 13:21:51 +03:00
parent 132deac5bc
commit 2980f050ed
21 changed files with 122 additions and 74 deletions

View file

@ -0,0 +1,6 @@
bump: minor
type: feat
**Renderer switches stay out of shipped games** — every `R3D_*` environment switch in
`ludic.render3d` (debug views, feature kills, file writers, hardware fakes, the Windows
feature overrides) now goes through one gate: a headless build honours them as before, a
windowed build only when `R3D_DEV` is set to anything but `0`. See `docs/SHIPPING.md`.

View file

@ -190,6 +190,28 @@ which is every `ludic run` - nothing mounts, no directory change happens, no
splash appears, and every open goes to the filesystem exactly as before.
Packing is a shipping step, and it is invisible until you ship.
### Renderer switches in a shipped game
`ludic.render3d` has about seventy `R3D_*` environment switches - debug views
(`R3D_WIRE`, `R3D_DEBUG*`, `R3D_LODDBG`, ...), feature kills (`R3D_NOSHADOW`,
`R3D_NOTERRAIN`, ...), file writers (`R3D_DUMP_*`, `R3D_DRAWSTATS_CSV`, ...),
hardware fakes (`R3D_CAPS`, `R3D_FORCE`, `R3D_GFX`) and the Windows feature
overrides (`R3D_DLSS*`, `R3D_REFLEX`, `R3D_HDR`, `R3D_MESH_GRASS`). They are
developer tools, so a player cannot trip them by accident:
- a **headless** build always honours them - tests, benchmarks and frame dumps
work exactly as before;
- a **windowed** build ignores every one of them unless `R3D_DEV` is set to
anything but `0`.
```bash
R3D_DEV=1 R3D_WIRE=1 ./MyGame.app/Contents/MacOS/MyGame
```
A game that has its own switches should gate them the same way. Inside render3d,
read a new switch with `r3d_env_has("R3D_FOO")` / `r3d_env("R3D_FOO")`, never
`Os.has_env` / `Os.env`.
## Signing and distribution
`ludic bundle` ad-hoc signs the result. On Apple silicon that is not optional

View file

@ -274,7 +274,7 @@ var ac_census_at: int = -2
function actor_draw() -> void {
if ac_actors == null { return }
ac_frame += 1
if ac_census_at == -2 { ac_census_at = -1; if Os.has_env("R3D_ACTOR_CENSUS") { ac_census_at = Text.to_int(Os.env("R3D_ACTOR_CENSUS")) } }
if ac_census_at == -2 { ac_census_at = -1; if r3d_env_has("R3D_ACTOR_CENSUS") { ac_census_at = Text.to_int(r3d_env("R3D_ACTOR_CENSUS")) } }
if ac_census_at > 0 and ac_frame == ac_census_at { actor_census() }
for i in 0 .. len(ac_actors) {
let a = ac_actors[i]

View file

@ -51,11 +51,11 @@ var ds_last: int = -1
function ds_enabled() -> bool {
if ds_on < 0 {
ds_on = 0
if Os.has_env("R3D_DRAWSTATS") {
if r3d_env_has("R3D_DRAWSTATS") {
ds_on = 1
ds_frames = Text.to_int(Os.env("R3D_DRAWSTATS"))
ds_frames = Text.to_int(r3d_env("R3D_DRAWSTATS"))
if ds_frames < 2 { ds_frames = 60 }
if Os.has_env("R3D_DRAWSTATS_FROM") { ds_from = Text.to_int(Os.env("R3D_DRAWSTATS_FROM")) }
if r3d_env_has("R3D_DRAWSTATS_FROM") { ds_from = Text.to_int(r3d_env("R3D_DRAWSTATS_FROM")) }
ds_pass_names = new []pointer; ds_pass_prog = new []long; ds_pass_tex = new []long
ds_row_pass = new []int; ds_row_prog = new []int; ds_row_kind = new []int
ds_row_draws = new []long; ds_row_inst = new []long; ds_row_idx = new []long
@ -188,7 +188,7 @@ function ds_report() -> void {
shown += 1
}
var path = "build/drawstats.csv"
if Os.has_env("R3D_DRAWSTATS_CSV") { path = Os.env("R3D_DRAWSTATS_CSV") }
if r3d_env_has("R3D_DRAWSTATS_CSV") { path = r3d_env("R3D_DRAWSTATS_CSV") }
let f = file_open(path, "wb")
if f == null { print(`r3d: drawstats could not write {path}`); return }
let head = "backend,width,height,frames,pass,kind,program,draws_per_frame,instances_per_frame,indices_per_frame,pass_program_changes_per_frame,pass_texture_binds_per_frame\n"

View file

@ -0,0 +1,19 @@
# env.ludic — the one gate every R3D_* switch goes through.
# The debug views, feature kills, file writers and hardware fakes are developer
# tools: a headless build (tests, benchmarks, frame dumps) always honours them, a
# windowed build - which is what a player runs - only when R3D_DEV is set to
# anything but "0". Read every R3D_* through r3d_env_has / r3d_env, never
# Os.has_env / Os.env.
var r3d_dev_v: int = -1
function r3d_dev() -> bool {
if r3d_dev_v < 0 { if Os.has_env("R3D_DEV") and Os.env("R3D_DEV") != "0" { r3d_dev_v = 1 } else { r3d_dev_v = 0 } }
return r3d_dev_v == 1
}
function r3d_env_has(name: string) -> bool {
if is_windowed() and not r3d_dev() { return false }
return Os.has_env(name)
}
function r3d_env(name: string) -> string {
if is_windowed() and not r3d_dev() { return "" }
return Os.env(name)
}

View file

@ -48,7 +48,7 @@ function gpu_name(kind: int) -> string {
# caller can show the player (gpu_fallback_reason).
function gpu_select() -> int {
if gpu_kind != 0 { return gpu_kind }
if Os.has_env("R3D_GFX") { gpu_wanted = gpu_kind_of(Os.env("R3D_GFX")) }
if r3d_env_has("R3D_GFX") { gpu_wanted = gpu_kind_of(r3d_env("R3D_GFX")) }
if gpu_wanted == 0 { gpu_wanted = GPU_GL }
gpu_kind = GPU_GL
if gpu_wanted == GPU_VK {
@ -318,7 +318,7 @@ function gpu_caps_fake(kind: string) -> void {
function gpu_caps_probe() -> void {
if gpu_cap_probed { return }
gpu_cap_probed = true
if Os.has_env("R3D_CAPS") { gpu_caps_fake(Os.env("R3D_CAPS")); return }
if r3d_env_has("R3D_CAPS") { gpu_caps_fake(r3d_env("R3D_CAPS")); return }
gpu_cap_windows = Os.platform() == "windows"
if not gpu_cap_windows { return }
if Vk.open() == 0 { return }
@ -793,7 +793,7 @@ function gpu_fb_at(fb: int) -> int {
return fb * GPU_FB_W
}
function gpu_glcheck_on() -> bool {
if gpu_glcheck < 0 { gpu_glcheck = 0; if Os.has_env("R3D_GLCHECK") { gpu_glcheck = 1 } }
if gpu_glcheck < 0 { gpu_glcheck = 0; if r3d_env_has("R3D_GLCHECK") { gpu_glcheck = 1 } }
return gpu_glcheck == 1
}
function gpu_check(tag: string) -> int { if gpu_kind == GPU_VK { return 0 }; return gl_check(tag) }

View file

@ -35,7 +35,7 @@ function gvk_note(msg: string) -> void {
print(msg)
if not gvk_errlog_read {
gvk_errlog_read = true
if Os.has_env("R3D_VK_ERRLOG") { gvk_errlog = Os.env("R3D_VK_ERRLOG") }
if r3d_env_has("R3D_VK_ERRLOG") { gvk_errlog = r3d_env("R3D_VK_ERRLOG") }
}
if gvk_errlog == null { return }
let f = file_open(gvk_errlog, "ab")
@ -205,7 +205,7 @@ function gvk_init() -> bool {
# mesh-shader grass: the extension, and its meshShader feature asked for where the device has it.
# R3D_NO_MESH=1 leaves it off.
gvk_has_mesh = false
if gvk_ext_in(dexts, nde, VK_EXT_MESH_SHADER_EXTENSION_NAME) and not Os.has_env("R3D_NO_MESH") {
if gvk_ext_in(dexts, nde, VK_EXT_MESH_SHADER_EXTENSION_NAME) and not r3d_env_has("R3D_NO_MESH") {
let fm = bytes(VkPhysicalDeviceMeshShaderFeaturesEXT_sizeof)
Vk.zero(fm, VkPhysicalDeviceMeshShaderFeaturesEXT_sizeof)
Vk.put_i32(fm, VkPhysicalDeviceMeshShaderFeaturesEXT_sType, VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_MESH_SHADER_FEATURES_EXT)

View file

@ -965,7 +965,7 @@ var gvk_has_hdr_meta: bool = false # the device took VK_EXT_hdr_metadata
# R3D_HDR=1 / 0 overrides the setting, for a desktop test
function r3d_hdr(on: bool) -> void {
var want = on
if Os.has_env("R3D_HDR") { want = Text.to_int(Os.env("R3D_HDR")) != 0 }
if r3d_env_has("R3D_HDR") { want = Text.to_int(r3d_env("R3D_HDR")) != 0 }
if want == gvk_hdr_want { return }
gvk_hdr_want = want
if gvk_swap != 0 { gvk_swap_stale = true }
@ -1420,7 +1420,7 @@ function gvk_open(w: int, h: int, title: string) -> bool {
}
if not gvk_frame_init() { return false }
if not gvk_screen_make(gl_w, gl_h) { return false }
if Os.has_env("R3D_VK_PROBE") { gvk_compute_probe() }
if r3d_env_has("R3D_VK_PROBE") { gvk_compute_probe() }
if is_windowed() { return gvk_swap_make(gl_w, gl_h) }
return true
}
@ -1825,7 +1825,7 @@ var gvk_n_flush: int = 0
var gvk_prof_frames: int = 0
var gvk_n_pipe_new: int = 0 # pipelines made this profile window
function gvk_prof() -> bool {
if gvk_prof_state < 0 { gvk_prof_state = 0; if Os.has_env("R3D_VK_PROF") { gvk_prof_state = 1 } }
if gvk_prof_state < 0 { gvk_prof_state = 0; if r3d_env_has("R3D_VK_PROF") { gvk_prof_state = 1 } }
return gvk_prof_state == 1
}
function gvk_prof_frame() -> void {

View file

@ -43,7 +43,7 @@ var grass_mesh_on: bool = false
var grass_mesh_prog: int = 0
function r3d_mesh_grass(on: bool) -> void {
grass_mesh_on = on
if Os.has_env("R3D_MESH_GRASS") { grass_mesh_on = Text.to_int(Os.env("R3D_MESH_GRASS")) != 0 }
if r3d_env_has("R3D_MESH_GRASS") { grass_mesh_on = Text.to_int(r3d_env("R3D_MESH_GRASS")) != 0 }
}
function grass_mesh_live() -> bool { return grass_mesh_on and grass_merge and grass_mesh_prog != 0 and gpu_has_mesh() }
@ -83,7 +83,7 @@ function grass_blade_mesh(rows: int) -> Mesh {
}
function grass_init() -> void {
grass_merge = gpu_has_mdi() and not Os.has_env("R3D_GRASS_TILES")
grass_merge = gpu_has_mdi() and not r3d_env_has("R3D_GRASS_TILES")
var defs = "#define FOLIAGE\n#define BLADE\n"
if grass_merge {
defs = defs + "#define TILES\n"
@ -98,9 +98,9 @@ function grass_init() -> void {
grass_s0 = fl(0.11)
grass_d0 = fi(45)
grass_radius = fi(1600)
if Os.has_env("R3D_NOBLADES") { grass_on = false }
if Os.has_env("R3D_GRASS_R") { grass_radius = fi(Text.to_int(Os.env("R3D_GRASS_R"))) }
if Os.has_env("R3D_GRASS_DBG") { grass_dbg = Text.to_int(Os.env("R3D_GRASS_DBG")) }
if r3d_env_has("R3D_NOBLADES") { grass_on = false }
if r3d_env_has("R3D_GRASS_R") { grass_radius = fi(Text.to_int(r3d_env("R3D_GRASS_R"))) }
if r3d_env_has("R3D_GRASS_DBG") { grass_dbg = Text.to_int(r3d_env("R3D_GRASS_DBG")) }
}
# indices per 16 m cell that could exist at distance d (the count the shader computes)

View file

@ -98,7 +98,7 @@ function overlay_init(font_dir: string) -> bool {
ov_white = tex_solid(255, 255, 255, 255)
overlay_font(font_dir)
if ov_font == 0 { print("overlay: no font atlas, text disabled") }
ov_dbg = Os.has_env("R3D_FONTDBG")
ov_dbg = r3d_env_has("R3D_FONTDBG")
ov_ready = true
return true
}

View file

@ -341,7 +341,7 @@ function post_tonemap(color_tex: int) -> void {
u_f(gpu_uniform(post_p_sharp, "u_amount"), post_sharpen)
# R3D_NOGRAIN=1: no film grain, so two frames of a still camera can be compared for what else moves
var grain = post_grain
if Os.has_env("R3D_NOGRAIN") { grain = F_ZERO }
if r3d_env_has("R3D_NOGRAIN") { grain = F_ZERO }
u_f(gpu_uniform(post_p_sharp, "u_grain"), grain)
u_f(gpu_uniform(post_p_sharp, "u_time"), r3d_time)
mesh_draw(post_fs)

View file

@ -25,7 +25,7 @@ var prof_active: int = -1 # slot whose query is currently open
var prof_scratch: words = null
function prof_init() -> void {
prof_on = Os.has_env("R3D_PROF")
prof_on = r3d_env_has("R3D_PROF")
if not prof_on { return }
prof_names = new []pointer
prof_ns = new []long

View file

@ -61,9 +61,9 @@ function r3d_shader_src(name: string, defines: string, is_frag: bool) -> string
# 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 { r3d_prog_log = 0; if r3d_env_has("R3D_PROGRAMS_LOG") { r3d_prog_log = 1 } }
if r3d_prog_log == 0 { return }
let f = file_open(Os.env("R3D_PROGRAMS_LOG"), "ab")
let f = file_open(r3d_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", ";")

View file

@ -2,6 +2,7 @@
# ludic.render3d — a physically based 3D renderer on Gl.* (OpenGL 4.1 core).
# Import this one file; the game supplies scene_draw() / scene_draw_casters().
# ============================================================================
import "env.ludic"
import "fmath.ludic"
import "gpu.ludic"
import "gpu_manifest.ludic"

View file

@ -30,7 +30,7 @@ var r3d_clip_y: int = 0xCF000000 # -2^31: no clipping
# R3D_NOPREPASS=1: light the foliage the old way, every card behind the front one included
var r3d_prepass_env: int = -1
function r3d_prepass_off() -> bool {
if r3d_prepass_env < 0 { r3d_prepass_env = 0; if Os.has_env("R3D_NOPREPASS") { r3d_prepass_env = 1 } }
if r3d_prepass_env < 0 { r3d_prepass_env = 0; if r3d_env_has("R3D_NOPREPASS") { r3d_prepass_env = 1 } }
return r3d_prepass_env == 1
}
function fog_bind(prog: int) -> void {
@ -40,7 +40,7 @@ function fog_bind(prog: int) -> void {
u_f(gpu_uniform(prog, "u_fog_height_falloff"), r3d_fog_falloff)
u_f(gpu_uniform(prog, "u_fog_base"), r3d_fog_base)
var cs = r3d_cloud_shadow
if Os.has_env("R3D_NOCLOUD") { cs = F_ZERO }
if r3d_env_has("R3D_NOCLOUD") { cs = F_ZERO }
u_f(gpu_uniform(prog, "u_cloud_shadow"), cs)
u_f(gpu_uniform(prog, "u_time"), r3d_time)
}
@ -52,18 +52,18 @@ var r3d_no_shadow: bool = false
var r3d_no_trees: bool = false
var r3d_no_refl: bool = false
function r3d_env_flags() -> void {
r3d_no_shadow = Os.has_env("R3D_NOSHADOW")
r3d_no_trees = Os.has_env("R3D_NOTREES")
r3d_no_refl = Os.has_env("R3D_NOREFL")
if Os.has_env("R3D_DEBUG") { r3d_debug = true }
if Os.has_env("R3D_DBGSHADOW") { r3d_debug_shadow = true }
if Os.has_env("R3D_NOGI") { post_gi_strength = F_ZERO; post_ao_strength = F_ZERO; post_no_gi = true }
if Os.has_env("R3D_MSAA") { post_ms_samples = Text.to_int(Os.env("R3D_MSAA")) }
sc_skip_blade = Os.has_env("R3D_NOBLADES")
sc_skip_card = Os.has_env("R3D_NOCARDS")
sc_dbg_lod = Os.has_env("R3D_LODDBG")
if Os.has_env("R3D_ANISO") {
let a = Text.to_int(Os.env("R3D_ANISO"))
r3d_no_shadow = r3d_env_has("R3D_NOSHADOW")
r3d_no_trees = r3d_env_has("R3D_NOTREES")
r3d_no_refl = r3d_env_has("R3D_NOREFL")
if r3d_env_has("R3D_DEBUG") { r3d_debug = true }
if r3d_env_has("R3D_DBGSHADOW") { r3d_debug_shadow = true }
if r3d_env_has("R3D_NOGI") { post_gi_strength = F_ZERO; post_ao_strength = F_ZERO; post_no_gi = true }
if r3d_env_has("R3D_MSAA") { post_ms_samples = Text.to_int(r3d_env("R3D_MSAA")) }
sc_skip_blade = r3d_env_has("R3D_NOBLADES")
sc_skip_card = r3d_env_has("R3D_NOCARDS")
sc_dbg_lod = r3d_env_has("R3D_LODDBG")
if r3d_env_has("R3D_ANISO") {
let a = Text.to_int(r3d_env("R3D_ANISO"))
tex_anisotropy = 1.0
if a >= 2 { tex_anisotropy = 2.0 }
if a >= 4 { tex_anisotropy = 4.0 }
@ -84,7 +84,7 @@ function r3d_open(w: int, h: int, title: string) -> bool {
r3d_env_flags()
gpu_select()
if not gpu_open(w, h, title) { print("r3d: no OpenGL context"); return false }
if Os.has_env("R3D_NOVSYNC") { gpu_vsync(0) }
if r3d_env_has("R3D_NOVSYNC") { gpu_vsync(0) }
var renderer: string = gpu_renderer_name()
print(`r3d: {gl_w}x{gl_h} on {renderer}`)
prof_init()
@ -167,7 +167,7 @@ function r3d_frame(time: int) -> void {
# or a fullscreen change does. Headless has no window to resize, and this path is where
# a stale attachment or a texture freed twice shows up.
r3d_test_frame += 1
if r3d_test_resize == 0 and Os.has_env("R3D_RESIZE_AT") { r3d_test_resize = Text.to_int(Os.env("R3D_RESIZE_AT")) }
if r3d_test_resize == 0 and r3d_env_has("R3D_RESIZE_AT") { r3d_test_resize = Text.to_int(r3d_env("R3D_RESIZE_AT")) }
# R3D_RESIZE_AT=<n>: from frame n on, rebuild every screen-sized buffer every few
# frames at a different size, as dragging a window edge or entering fullscreen does.
if r3d_test_resize > 0 and r3d_test_frame >= r3d_test_resize and r3d_test_frame % 4 == 0 {
@ -188,7 +188,7 @@ function r3d_frame(time: int) -> void {
cam_begin_frame(post_frame, gl_w, gl_h)
# R3D_CAM_LOG=1: the camera each frame, in millimetres and thousandths of a radian - to tell a
# camera that moves while the hiker stands still from a picture that shakes on its own
if r3d_cam_log < 0 { r3d_cam_log = 0; if Os.has_env("R3D_CAM_LOG") { r3d_cam_log = 1 } }
if r3d_cam_log < 0 { r3d_cam_log = 0; if r3d_env_has("R3D_CAM_LOG") { r3d_cam_log = 1 } }
if r3d_cam_log == 1 {
print(`cam {r3d_test_frame} pos {f_to_int(f_mul(cam_pos[0], fi(1000)))} {f_to_int(f_mul(cam_pos[1], fi(1000)))} {f_to_int(f_mul(cam_pos[2], fi(1000)))} yaw {f_to_int(f_mul(cam_yaw, fi(1000)))} pitch {f_to_int(f_mul(cam_pitch, fi(1000)))} jitter {f_to_int(f_mul(gsl_jitter_x, fi(1000000)))} {f_to_int(f_mul(gsl_jitter_y, fi(1000000)))} render {post_w}x{post_h} reset {gsl_reset} evalok {gsl_eval_ok} fresh {gsl_fresh}`)
}

View file

@ -356,7 +356,7 @@ var sc_a2c: bool = true
function scatter_init() -> void {
# R3D_DUMP_ATLAS: every impostor and card atlas the run bakes, to build/atlas_<n>_{color,alpha}.ppm
sc_debug_dump = Os.has_env("R3D_DUMP_ATLAS")
sc_debug_dump = r3d_env_has("R3D_DUMP_ATLAS")
sc_prog = r3d_program("model.vert", "model.frag", "")
sc_prog_fol = r3d_program("model.vert", "model.frag", "#define FOLIAGE\n#define WIND\n#define ALPHA_TEST\n")
sc_prog_fol_depth = r3d_program("model.vert", "depth.frag", "#define FOLIAGE\n#define WIND\n#define ALPHA_TEST\n")
@ -646,7 +646,7 @@ function layer_gpu_prepare(l: Layer) -> bool {
# Os.env is null when the variable is unset, and a compare reads through it: ask first
# on by default wherever there is compute; R3D_GPU_CULL=0 keeps the CPU partition (for comparing)
var off = false
if Os.has_env("R3D_GPU_CULL") { off = Os.env("R3D_GPU_CULL") == "0" }
if r3d_env_has("R3D_GPU_CULL") { off = r3d_env("R3D_GPU_CULL") == "0" }
if gpu_has_compute() and gpu_has_mdi() and not off {
sc_cull_prog = gpu_compute("scatter_cull", 4)
if sc_cull_prog > 0 { print("r3d: scatter: tree layers are culled on the GPU") }
@ -968,7 +968,7 @@ function layer_level_casts_here(l: Layer, k: int) -> bool {
# tall, put a shadow on anything the cascade being rendered covers? The flowers' levels ask it
# (layer_level_casts_here), and so does every actor (actor_draw_casters).
function cast_band_reaches(dmin: int, dmax: int, height: int) -> bool {
if sc_cast_all < 0 { sc_cast_all = 0; if Os.has_env("R3D_CAST_ALL") { sc_cast_all = 1 } }
if sc_cast_all < 0 { sc_cast_all = 0; if r3d_env_has("R3D_CAST_ALL") { sc_cast_all = 1 } }
if sc_cast_all == 1 or sh_split == null { return true }
if sc_cast_gen != sc_view_gen {
sc_cast_gen = sc_view_gen

View file

@ -277,7 +277,7 @@ function shadow_bind(prog: int) -> void {
u_fv(sh_loc(prog, "u_cascade_split"), SHADOW_CASCADES, sh_split)
if r3d_debug_shadow and not sh_printed3 { sh_printed3 = true; print(`range {f_fx(sh_range[0])} {f_fx(sh_range[1])} {f_fx(sh_range[2])} {f_fx(sh_range[3])} texel*1000 {f_fx(f_mul(sh_texel[0], fi(1000)))} {f_fx(f_mul(sh_texel[1], fi(1000)))} {f_fx(f_mul(sh_texel[2], fi(1000)))} {f_fx(f_mul(sh_texel[3], fi(1000)))} locs {gpu_uniform(prog, "u_cascade_range")} {gpu_uniform(prog, "u_cascade_texel")}`) }
u_fv(sh_loc(prog, "u_cascade_range"), SHADOW_CASCADES, sh_range)
if Os.has_env("R3D_FORCE") { sh_force = Text.to_int(Os.env("R3D_FORCE")) }
if r3d_env_has("R3D_FORCE") { sh_force = Text.to_int(r3d_env("R3D_FORCE")) }
u_i(gpu_uniform(prog, "u_force_cascade"), sh_force)
u_fv(sh_loc(prog, "u_cascade_texel"), SHADOW_CASCADES, sh_texel)
}

View file

@ -54,8 +54,8 @@ var stream_walks: int = 0 # streams that walked their whole ring this f
function stream_new(layer: Layer, size: int, reach: int, b0: int, b1: int, b2: int, b3: int) -> Stream {
if not stream_cap_read {
stream_cap_read = true
if Os.has_env("R3D_STREAM_CAP") { STREAM_MAX_CHUNKS = Text.to_int(Os.env("R3D_STREAM_CAP")) }
stream_no_evict = Os.has_env("R3D_NOEVICT")
if r3d_env_has("R3D_STREAM_CAP") { STREAM_MAX_CHUNKS = Text.to_int(r3d_env("R3D_STREAM_CAP")) }
stream_no_evict = r3d_env_has("R3D_NOEVICT")
}
let s = new Stream
s.layer = layer; s.size = size; s.reach = reach

View file

@ -76,7 +76,7 @@ function gsl_fn(feature: int, name: string) -> pointer {
# Before Vk.open(): ask the loader for the interposer. R3D_NO_STREAMLINE=1 keeps plain Vulkan.
function gsl_boot() -> void {
if Os.platform() != "windows" { return }
if Os.has_env("R3D_NO_STREAMLINE") { return }
if r3d_env_has("R3D_NO_STREAMLINE") { return }
Vk.sl_prefer(1)
}
@ -91,10 +91,10 @@ function gsl_init() -> void {
gsl_header(pref, 0x1ca1, 0x0965, 0xbf8e, 0x432b, 0x8da1, 0x6716, 0xd879, 0xfb14, 1)
# logLevel eOff; R3D_SL_LOG=<directory> writes Streamline's own verbose log (sl.log) there
var level = 0
if Os.has_env("R3D_SL_LOG") {
if r3d_env_has("R3D_SL_LOG") {
level = 2
let dir: pointer = Os.env("R3D_SL_LOG")
let n = Text.length(Os.env("R3D_SL_LOG"))
let dir: pointer = r3d_env("R3D_SL_LOG")
let n = Text.length(r3d_env("R3D_SL_LOG"))
gsl_logdir = gsl_struct(n * 2 + 8) # pathToLogsAndData is a wide string
for i in 0 .. n { Vk.put_i32(gsl_logdir, i * 2, dir[i] & 255) }
Vk.put_ptr(pref, 56, gsl_logdir)
@ -159,7 +159,7 @@ var gsl_f_marker: pointer = null
function r3d_reflex(mode: int) -> void {
gsl_reflex_mode = mode
if Os.has_env("R3D_REFLEX") { gsl_reflex_mode = Text.to_int(Os.env("R3D_REFLEX")) }
if r3d_env_has("R3D_REFLEX") { gsl_reflex_mode = Text.to_int(r3d_env("R3D_REFLEX")) }
}
function r3d_reflex_live() -> bool { return gsl_on and gsl_reflex_ok and gsl_reflex_mode > 0 }
@ -248,7 +248,7 @@ var gsl_eval_ok: bool = true # the last evaluate worked: only then is th
# R3D_DLSS=0..4 overrides the setting, for a headless take
function r3d_dlss(mode: int) -> void {
var m = mode
if Os.has_env("R3D_DLSS") { m = Text.to_int(Os.env("R3D_DLSS")) }
if r3d_env_has("R3D_DLSS") { m = Text.to_int(r3d_env("R3D_DLSS")) }
if m != gsl_dlss_mode { gsl_reset = true }
gsl_dlss_mode = m
}
@ -279,7 +279,7 @@ function gsl_fill_options(w: int, h: int) -> void {
# K's 2.8 - slower than no DLSS at all (33 fps against 41; with K, 60). R3D_DLSS_PRESET=<n>
# (sl::DLSSPreset: 11 K, 12 L, 13 M) sets every mode's, to measure them against each other.
var preset = 11
if Os.has_env("R3D_DLSS_PRESET") { preset = Text.to_int(Os.env("R3D_DLSS_PRESET")) }
if r3d_env_has("R3D_DLSS_PRESET") { preset = Text.to_int(r3d_env("R3D_DLSS_PRESET")) }
if preset > 0 { for f in 0 .. 6 { Vk.put_i32(gsl_opts, 60 + f * 4, preset) } } # dlaa, quality, balanced, performance, ultra performance, ultra quality
}
@ -417,8 +417,8 @@ function gsl_constants() -> void {
var jx = f_neg(gsl_jpx)
var jy = f_neg(gsl_jpy)
# R3D_DLSS_JX / R3D_DLSS_JY = -1 flip a sign, to check the convention against the picture
if Os.has_env("R3D_DLSS_JX") and Text.to_int(Os.env("R3D_DLSS_JX")) < 0 { jx = f_neg(jx) }
if Os.has_env("R3D_DLSS_JY") and Text.to_int(Os.env("R3D_DLSS_JY")) < 0 { jy = f_neg(jy) }
if r3d_env_has("R3D_DLSS_JX") and Text.to_int(r3d_env("R3D_DLSS_JX")) < 0 { jx = f_neg(jx) }
if r3d_env_has("R3D_DLSS_JY") and Text.to_int(r3d_env("R3D_DLSS_JY")) < 0 { jy = f_neg(jy) }
Vk.put_i32(k, 352, jx)
Vk.put_i32(k, 356, jy)
Vk.put_i32(k, 360, F_ONE) # mvecScale

View file

@ -384,7 +384,7 @@ function terrain_load_textures() -> void {
ter_tex[12] = tex_load(a + "aerial_grass_rock_disp_2k.png", false)
ter_tex[13] = tex_load(a + "cliff_side_diff_2k.png", true)
ter_tex[14] = tex_load(a + "cliff_side_nor_gl_2k.png", false)
if Os.has_env("R3D_TEXDBG") { for i in 0 .. 15 { print(`ter_tex[{string(i)}] = {string(ter_tex[i])}`) } }
if r3d_env_has("R3D_TEXDBG") { for i in 0 .. 15 { print(`ter_tex[{string(i)}] = {string(ter_tex[i])}`) } }
}
function terrain_init() -> void {
@ -404,16 +404,16 @@ function terrain_init_step(i: int) -> void {
function terrain_init_finish() -> void {
cdlod_init()
ter_wire = Os.has_env("R3D_WIRE")
ter_force_far = Os.has_env("R3D_TFARONLY")
ter_no_split = Os.has_env("R3D_NOSPLIT")
ter_force_near = Os.has_env("R3D_TNEARONLY")
ter_skip = Os.has_env("R3D_NOTERRAIN")
ter_wire = r3d_env_has("R3D_WIRE")
ter_force_far = r3d_env_has("R3D_TFARONLY")
ter_no_split = r3d_env_has("R3D_NOSPLIT")
ter_force_near = r3d_env_has("R3D_TNEARONLY")
ter_skip = r3d_env_has("R3D_NOTERRAIN")
var defs = ""
if r3d_debug_shadow { defs = "#define DEBUG_SHADOW\n" }
if Os.has_env("R3D_DEBUG_MAT") { defs = "#define DEBUG_MAT\n" }
if Os.has_env("R3D_DEBUG_NRM") { defs = "#define DEBUG_NRM\n" }
if Os.has_env("R3D_DEBUG_ALB") { defs = "#define DEBUG_ALB\n" }
if r3d_env_has("R3D_DEBUG_MAT") { defs = "#define DEBUG_MAT\n" }
if r3d_env_has("R3D_DEBUG_NRM") { defs = "#define DEBUG_NRM\n" }
if r3d_env_has("R3D_DEBUG_ALB") { defs = "#define DEBUG_ALB\n" }
# Elimination profiling. Measure these by FRAME TIME (prof_ft_report), not by the
# per-pass GPU timers: this driver's timer queries attribute a pass's fragment work
# almost arbitrarily, and will happily report a pass at a tenth of its cost.
@ -423,20 +423,20 @@ function terrain_init_finish() -> void {
# R3D_NOTERRAIN skips the ground entirely (what it costs); R3D_TNEARONLY / R3D_TFARONLY
# draw every patch with one tier's program (what each tier costs over a whole frame);
# R3D_NOSPLIT goes back to the single program that holds both tiers.
if Os.has_env("R3D_TFAST") { defs = defs + "#define TFAST_" + Os.env("R3D_TFAST") + "\n" }
if r3d_env_has("R3D_TFAST") { defs = defs + "#define TFAST_" + r3d_env("R3D_TFAST") + "\n" }
# Vulkan reads the cascades inside the ground's own shader and draws no separate sun pass
# (terrain.frag, SUN_INLINE): one rasterisation of the patches instead of two, in the frame and in
# the reflection. R3D_SUN_PASS=1 keeps the pass there too, for comparing.
ter_sun_inline = gpu_kind == GPU_VK and not Os.has_env("R3D_SUN_PASS")
ter_sun_inline = gpu_kind == GPU_VK and not r3d_env_has("R3D_SUN_PASS")
if ter_sun_inline { defs = defs + "#define SUN_INLINE\n" }
ter_prog = r3d_program("terrain.vert", "terrain.frag", defs)
ter_prog_far = r3d_program("terrain.vert", "terrain.frag", defs + "#define FAR_ONLY\n")
ter_prog_near = r3d_program("terrain.vert", "terrain.frag", defs + "#define NEAR_ONLY\n")
ter_sun_prog = r3d_program("terrain.vert", "tersun.frag", "")
ter_far_split = fi(200)
if Os.has_env("R3D_TFAR") { ter_far_split = fi(Text.to_int(Os.env("R3D_TFAR"))) }
if r3d_env_has("R3D_TFAR") { ter_far_split = fi(Text.to_int(r3d_env("R3D_TFAR"))) }
ter_far_band = fi(60)
if Os.has_env("R3D_TBAND") { ter_far_band = fi(Text.to_int(Os.env("R3D_TBAND"))) }
if r3d_env_has("R3D_TBAND") { ter_far_band = fi(Text.to_int(r3d_env("R3D_TBAND"))) }
ter_snow_line = fi(880)
gpu_check("terrain init")
}
@ -563,7 +563,7 @@ function terrain_sun_pass(w: int, h: int, depth: int) -> Target {
gpu_mesh_bind(cd_mesh)
cdlod_select(CD_LEVELS - 1, 0, 0)
ter_sun_pass = false
if Os.has_env("R3D_DUMP_SUN") and not ter_sun_dumped and not ter_reflect { ter_sun_dumped = true; tex_dump(t.color, w, h, "build/dbg_sun.ppm") }
if r3d_env_has("R3D_DUMP_SUN") and not ter_sun_dumped and not ter_reflect { ter_sun_dumped = true; tex_dump(t.color, w, h, "build/dbg_sun.ppm") }
return t
}
@ -645,7 +645,7 @@ function cdlod_init() -> void {
cd_mesh = m
cd_range = words(CD_LEVELS)
var r = fi(48)
if Os.has_env("R3D_CD_R0") { r = fi(Text.to_int(Os.env("R3D_CD_R0"))) }
if r3d_env_has("R3D_CD_R0") { r = fi(Text.to_int(r3d_env("R3D_CD_R0"))) }
for l in 0 .. CD_LEVELS { cd_range[l] = r; r = f_mul(r, F_TWO) }
cdlod_bounds()
}

View file

@ -37,7 +37,7 @@ var wb_primary: int = -1 # the body the reflection pass mirrors, o
function water_reflection_pass() -> void {
if wb_primary < 0 { return } # no body reflects: nothing to mirror
if water_refl == null {
if Os.has_env("R3D_REFLDIV") { water_refl_div = Text.to_int(Os.env("R3D_REFLDIV")) }
if r3d_env_has("R3D_REFLDIV") { water_refl_div = Text.to_int(r3d_env("R3D_REFLDIV")) }
if water_refl != null { target_free(water_refl) }
# sized from the scene target, not the window: with a render scale below 1 the frame
# this reflection is composited into is smaller than the drawable, and a reflection
@ -89,7 +89,7 @@ function water_reflection_pass() -> void {
sc_skip_blade = sb
sc_freeze = false
gpu_cull_face(GL_BACK)
if Os.has_env("R3D_DUMP_REFL") and not water_dumped { water_dumped = true; tex_dump(water_refl.color, water_refl.w, water_refl.h, "build/dbg_refl.ppm") }
if r3d_env_has("R3D_DUMP_REFL") and not water_dumped { water_dumped = true; tex_dump(water_refl.color, water_refl.w, water_refl.h, "build/dbg_refl.ppm") }
# restore
r3d_clip_y = 0xCF000000
m4_copy(cam_view, water_saved)
@ -118,10 +118,10 @@ var wb_dbg: int = -1
var wb_build_us: long = 0 # how long the last water_cells_build took (R3D_REFL_DBG prints it)
function water_reflect_visible() -> bool {
if wb_primary < 0 { return false }
if wb_refl_always < 0 { wb_refl_always = 0; if Os.has_env("R3D_REFL_ALWAYS") { wb_refl_always = 1 } }
if wb_refl_always < 0 { wb_refl_always = 0; if r3d_env_has("R3D_REFL_ALWAYS") { wb_refl_always = 1 } }
if wb_refl_always == 1 or ter_heights == null { return true }
if wb_ncells < 0 { let t0 = gl_now_us(); water_cells_build(); wb_build_us = gl_now_us() - t0 }
if wb_dbg < 0 { wb_dbg = 0; if Os.has_env("R3D_REFL_DBG") { wb_dbg = 1 } }
if wb_dbg < 0 { wb_dbg = 0; if r3d_env_has("R3D_REFL_DBG") { wb_dbg = 1 } }
if wb_dbg == 1 {
# R3D_REFL_DBG: once, what the test is made of and how much of it is in view
wb_dbg = 2