feat(render3d): load steps for a loading screen, four quality switches, and the ring's uniform usage

- r3d_open opens the window with nothing baked; r3d_load_count / r3d_load_step run the rest (sky
  and light, terrain, shadow and screen targets, cover and actors, grass) so a game can present a
  loading frame between them. r3d_init is the same calls in a row.
- sky_set_quality(width): the prefiltered sky light at 256 / 512 / 1024, baked again.
- post_set_msaa(samples): multisampling on OpenGL, remade in place (post_msaa_live is false on
  Vulkan); post_free frees the multisampled framebuffer too.
- STREAM_BUDGET_US is a variable; r3d_fog_scale multiplies the fog the day sets.
- Vulkan buffers carry UNIFORM_BUFFER usage: the frame's ring is bound as uniform buffers and was
  created without it (VUID-VkWriteDescriptorSet-descriptorType-00330, found on MoltenVK).

OpenGL frames at the five viewpoints unchanged; the game's 59 self-tests pass.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
Orkun ÇAKILKAYA 2026-09-15 14:46:55 +03:00
parent b9eda646fc
commit f2b976c976
8 changed files with 89 additions and 21 deletions

View file

@ -0,0 +1,11 @@
bump: minor
type: feat
**More of the renderer's quality is a switch a game can offer.**
- `sky_set_quality(width)`: the prefiltered sky light at 256, 512 or 1024 wide (half as tall),
baked again at once.
- `post_set_msaa(samples)`: 1, 2 or 4 samples for the scene on OpenGL, remaking the screen targets in
place (`post_msaa_live()` is false on Vulkan, which has no sample counts yet); `post_free` now
frees the multisampled framebuffer too.
- `STREAM_BUDGET_US` is a variable: the microseconds a frame may spend generating streamed cover.
- `r3d_fog_scale`: a multiplier over the fog density the daylight and the weather set.

View file

@ -0,0 +1,7 @@
bump: minor
type: feat
**A loading screen can show the renderer's start-up as it happens.** `r3d_open(w, h, title)` opens
the window and the graphics backend with nothing baked, and `r3d_load_count()` / `r3d_load_step(i)`
run the rest - the sky and its light, the terrain, the shadow and screen targets, the scattered cover
and actors, the grass - one step at a time, so a game can draw and present a frame between them.
`r3d_init` is those same calls in a row and is unchanged for everything that uses it.

View file

@ -143,7 +143,7 @@ function daylight_set(hours: int) -> void {
let dim = f_sub(F_ONE, f_mul(fl(0.55), oc))
for i in 0 .. 3 { day_ibl[i] = f_mul(f_lerp(day_ibl[i], grey, f_mul(fl(0.7), oc)), dim) }
if day_fog_base == 0 { day_fog_base = r3d_fog_density }
r3d_fog_density = f_mul(day_fog_base, day_fog_mul)
r3d_fog_density = f_mul(f_mul(day_fog_base, day_fog_mul), r3d_fog_scale)
# exposure: auto-exposure must not turn the night into day
# the ceiling has to move with the moon or auto-exposure eats the difference between a
# full-moon night and a new-moon one

View file

@ -585,7 +585,7 @@ function gvk_buf_reserve(b: int, n: int) -> bool {
Vk.zero(bci, VkBufferCreateInfo_sizeof)
Vk.put_i32(bci, VkBufferCreateInfo_sType, VK_STRUCTURE_TYPE_BUFFER_CREATE_INFO)
Vk.put_i64(bci, VkBufferCreateInfo_size, size_l)
Vk.put_i32(bci, VkBufferCreateInfo_usage, VK_BUFFER_USAGE_VERTEX_BUFFER_BIT | VK_BUFFER_USAGE_INDEX_BUFFER_BIT | VK_BUFFER_USAGE_TRANSFER_DST_BIT | VK_BUFFER_USAGE_STORAGE_BUFFER_BIT | VK_BUFFER_USAGE_INDIRECT_BUFFER_BIT)
Vk.put_i32(bci, VkBufferCreateInfo_usage, VK_BUFFER_USAGE_VERTEX_BUFFER_BIT | VK_BUFFER_USAGE_INDEX_BUFFER_BIT | VK_BUFFER_USAGE_TRANSFER_DST_BIT | VK_BUFFER_USAGE_STORAGE_BUFFER_BIT | VK_BUFFER_USAGE_INDIRECT_BUFFER_BIT | VK_BUFFER_USAGE_UNIFORM_BUFFER_BIT)
Vk.put_i32(bci, VkBufferCreateInfo_sharingMode, VK_SHARING_MODE_EXCLUSIVE)
let out = bytes(8)
var r = Vk.create_buffer(gvk_dev, bci, null, out)

View file

@ -50,11 +50,28 @@ var post_grain: int = 0
# the screen-sized targets go away before post_init makes them at a new size
function post_free() -> void {
if post_hdr == null { return }
if post_ms_fbo != 0 { gpu_fb_free(post_ms_fbo); post_ms_fbo = 0 }
target_free(post_hdr); target_free(post_ao); target_free(post_ao_blur); target_free(post_ldr)
target_free(post_depth_copy); target_free(post_prev); target_free(post_scene)
for i in 0 .. len(post_bloom) { target_free(post_bloom[i]) }
post_hdr = null
}
# Multisampled scene: 1 (temporal AA alone), 2 or 4. OpenGL remakes the scene targets at once;
# the Vulkan backend has no sample counts yet and keeps drawing single-sampled (post_msaa_live says so).
function post_msaa_live() -> bool { return gpu_is_gl() }
function post_set_msaa(n: int) -> void {
var want = n
if want < 1 { want = 1 }
if not post_msaa_live() { want = 1 }
if want == post_ms_samples { return }
post_ms_samples = want
if post_hdr != null {
let w = post_w; let h = post_h
post_free()
post_init(w, h)
}
}
function post_init(w: int, h: int) -> void {
post_w = w; post_h = h
post_hdr = target_new(w, h, GL_RGBA16F, GL_RGBA, GL_HALF_FLOAT, true, GL_LINEAR)

View file

@ -7,6 +7,7 @@
var r3d_sky_prog: int = 0
var r3d_fog_density: int = 0
var r3d_fog_scale: int = 0x3F800000 # float bits: a setting's multiplier over the density the day sets
var r3d_fog_falloff: int = 0
var r3d_fog_base: int = 0 # the height fog is measured from (float bits); 0 = y = 0
var r3d_time: int = 0
@ -70,7 +71,16 @@ function r3d_env_flags() -> void {
if a >= 16 { tex_anisotropy = 16.0 }
}
}
# Start the renderer in one call: the window, then every load step in order. A game that shows
# a loading screen calls r3d_open, draws its screen, and runs r3d_load_step itself between frames.
function r3d_init(w: int, h: int, title: string) -> bool {
if not r3d_open(w, h, title) { return false }
for i in 0 .. r3d_load_count() { if not r3d_load_step(i) { return false } }
return true
}
# the window and the graphics backend; nothing is baked yet, but a frame can be presented
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 }
@ -79,21 +89,35 @@ function r3d_init(w: int, h: int, title: string) -> bool {
print(`r3d: {gl_w}x{gl_h} on {renderer}`)
prof_init()
cam_init(fr(gl_w, gl_h))
if not sky_load(r3d_assets + "/hdri/kloofendal_48d_partly_cloudy_puresky_4k.hdr") { return false }
daylight_init()
if r3d_dem_path != null { terrain_use_dem(r3d_dem_path, r3d_dem_min, r3d_dem_max, r3d_dem_base, r3d_dem_ox, r3d_dem_oz) }
if r3d_ortho_path != null { terrain_use_ortho(r3d_ortho_path) }
terrain_init()
shadow_init()
post_init(gl_w, gl_h)
scatter_init()
actor_init()
grass_init()
r3d_sky_prog = r3d_program("fullscreen.vert", "sky.frag", "")
r3d_fog_density = fl(0.00014)
r3d_fog_falloff = fl(0.002)
r3d_ready = true
gpu_check("r3d init")
return true
}
# The load, as steps a loading screen can show between: 0 the sky and its image-based light,
# 1 the terrain (height, normals, shadow, materials), 2 the shadow maps and the screen targets,
# 3 the scattered cover and the actors, 4 the grass and the sky pass. The order is r3d_init's.
function r3d_load_count() -> int { return 5 }
function r3d_load_step(i: int) -> bool {
if i == 0 {
if not sky_load(r3d_assets + "/hdri/kloofendal_48d_partly_cloudy_puresky_4k.hdr") { return false }
daylight_init()
} else if i == 1 {
if r3d_dem_path != null { terrain_use_dem(r3d_dem_path, r3d_dem_min, r3d_dem_max, r3d_dem_base, r3d_dem_ox, r3d_dem_oz) }
if r3d_ortho_path != null { terrain_use_ortho(r3d_ortho_path) }
terrain_init()
} else if i == 2 {
shadow_init()
post_init(gl_w, gl_h)
} else if i == 3 {
scatter_init()
actor_init()
} else if i == 4 {
grass_init()
r3d_sky_prog = r3d_program("fullscreen.vert", "sky.frag", "")
r3d_fog_density = fl(0.00014)
r3d_fog_falloff = fl(0.002)
r3d_ready = true
gpu_check("r3d init")
}
return true
}

View file

@ -85,6 +85,15 @@ function sky_convolve(prog: int, target_tex: int, layer: int, w: int, h: int, ro
gpu_fb_free(fbo)
}
# the prefiltered specular's width per level (its height is half): 256, 512 or 1024. The image
# the reflections and the rough sheen are lit from; sky_set_quality bakes it again at a new size.
var sky_prefilter_w: int = 512
function sky_set_quality(w: int) -> void {
if w < 64 or w == sky_prefilter_w { return }
sky_prefilter_w = w
if sky_irradiance != 0 { sky_precompute() }
}
function sky_precompute() -> void {
gpu_depth_test(false)
if sky_irradiance != 0 {
@ -99,17 +108,17 @@ function sky_precompute() -> void {
gpu_tex_bind(GPU_TEX2D, sky_irradiance)
gpu_tex_param(GPU_TEX2D, GL_TEXTURE_WRAP_S, GL_REPEAT)
sky_convolve(p_irr, sky_irradiance, -1, 128, 64, F_ZERO)
# prefiltered specular: 6 roughness levels, 512 x 256 each, as a 2D array
# prefiltered specular: 6 roughness levels, sky_prefilter_w x half each, as a 2D array
let p_pre = sky_p_pre
sky_prefilter = gpu_tex_new()
gpu_tex_bind(GPU_TEX2D_ARRAY, sky_prefilter)
gpu_tex_image3d(GL_RGB16F, 512, 256, SKY_PREFILTER_LEVELS, GL_RGB, GL_FLOAT, null)
gpu_tex_image3d(GL_RGB16F, sky_prefilter_w, sky_prefilter_w / 2, SKY_PREFILTER_LEVELS, GL_RGB, GL_FLOAT, null)
gpu_tex_param(GPU_TEX2D_ARRAY, GL_TEXTURE_WRAP_S, GL_REPEAT)
gpu_tex_param(GPU_TEX2D_ARRAY, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE)
gpu_tex_param(GPU_TEX2D_ARRAY, GL_TEXTURE_MAG_FILTER, GL_LINEAR)
gpu_tex_param(GPU_TEX2D_ARRAY, GL_TEXTURE_MIN_FILTER, GL_LINEAR)
for l in 0 .. SKY_PREFILTER_LEVELS {
sky_convolve(p_pre, sky_prefilter, l, 512, 256, fr(l, SKY_PREFILTER_LEVELS - 1))
sky_convolve(p_pre, sky_prefilter, l, sky_prefilter_w, sky_prefilter_w / 2, fr(l, SKY_PREFILTER_LEVELS - 1))
}
# BRDF LUT
let p_brdf = sky_p_brdf

View file

@ -143,7 +143,7 @@ function stream_band(s: Stream, d: int) -> int {
# by band and kind. With a real microsecond clock the budget can just be the thing we
# actually care about — how long this frame is allowed to spend growing ground cover.
# Overshoot is bounded by one chunk, so keep chunks small on the dense near streams.
const STREAM_BUDGET_US: int = 2500 # microseconds of generation per frame
var STREAM_BUDGET_US: int = 2500 # microseconds of generation per frame; a setting may move it
var stream_deadline: long = 0
const STREAM_BUDGET: int = 8000 # kept for the work counter only
# The worst frame is now bounded by one chunk, not by the budget: stream_fill emits a