Skip to content

fix(scene): tint sub-worldviews and stop paying per ship - #22

Open
dennisdevulder wants to merge 2 commits into
mainfrom
fix/sub-worldview-shared-pipelines-entity-tint
Open

fix(scene): tint sub-worldviews and stop paying per ship#22
dennisdevulder wants to merge 2 commits into
mainfrom
fix/sub-worldview-shared-pipelines-entity-tint

Conversation

@dennisdevulder

@dennisdevulder dennisdevulder commented Aug 30, 2026

Copy link
Copy Markdown
Owner

Other players' ships rendered in full detail where stock shows a flat blue silhouette. That's fixed here. The PR also cuts what a sub-worldview costs to spawn — but see the last section, that is not the fix for the salvage-area FPS complaint that prompted this.

1. The entity tint was never read

The engine puts an HSL override on each sub-worldview's own Scene: getOverrideHue/Saturation/Luminance/Amount. Stock feeds those four bytes to its entityTint uniform for every sub-worldview draw, static and dynamic (GpuPlugin.java:833,1088, VAO.java:123), resets it to zero for the top level (:978,1007), and lerps in HSL component space in vert.glsl:88. That override is what makes another player's ship a flat silhouette. We never read it, so ships drew at full detail.

scene.vert now does the same lerp from a packed int. Two details ported deliberately from stock:

  • the lerp runs on the decoded HSL components before hslToRgb, with amount kept as a signed byte;
  • fHsl stays untinted for textured faces (it only drives texture lighting) and tinted for untextured ones, so a tinted ship still shows its textures rather than becoming a solid block of colour.

Stock also drops per-face alpha sorting for tinted scenes (GpuPlugin.java:1101). We sort at capture rather than at draw, so there is no equivalent knob to turn off.

Push constant repack

The scene push block was already at 128 bytes, which is what maxPushConstantsSize guarantees and what RADV and Intel actually report, so the tint had to make room. Vulkan allows one range per stage, so the split moved:

before after
vertex 0..95 — mat4, fogVtx, misc 0..99 — mat4, fogVtx, misc, entityTint
fragment 96..127fogFrag vec4, fragExtras vec4 100..123 — fogR/G/B, brightness, colorBlindIntensity, modes

textureLightMode, colorBlindMode, alphaMode and smoothBanding are all small flags and now share one modes int, replacing the previous smoothBanding + 10 * alphaMode encoding. Offsets live in ScenePipeline.VERT_PUSH_BYTES/FRAG_PUSH_BYTES so the layout has one source of truth.

The record signature collapsed from seventeen parameters to (cmd, mvp, VulkanFrameContext, SceneEntity) instead of growing an eighteenth. SceneEntity carries the per-scene state — placement and tint — with TOP_LEVEL as the identity.

2. Sub-worldview spawn cost

SubWorldViewManager.preScene constructed a SceneRenderer per worldview, lazily, inside the draw callback. Each one meant six ScenePipeline objects (each loading the SPIR-V, creating two shader modules, a descriptor set layout, a pipeline layout and compiling a pipeline), a descriptor pool and set that only ever bind the texture array and the shared animation UBO, a 12.5 MB arena preferentially in BAR memory plus a VRAM mirror on non-ReBAR machines, and a vkDeviceWaitIdle for the first capture — all of it every time a ship sailed into view.

  • ScenePipelines owns the six pipelines and the descriptor set, built once per device and shared by the top level and every sub-worldview. SceneVertexBuffer loses its descriptor pool entirely.
  • Arenas are seeded small and grown by the loop captureScene already runs, so ship size is measured rather than guessed.
  • The device drain is skipped until a renderer has recorded a draw — nothing the GPU could be executing references an arena it has never drawn from.
  • Zone loops are bounded by the captured extent instead of the top level's 23×23 grid, and the frustum plane scratch is preallocated.
  • Per-scene capture logging drops to debug for sub-worldviews, and an overflow the growth loop is about to fix no longer logs a warning.

This removes hitches when ships spawn and a lot of memory. It is worth having on its own terms.

What this does NOT fix

The complaint that started this was FPS in a salvage area with many boats. Three-way measurement at the same spot says that is engine-side:

condition FPS
normal, we draw the ships 50
-Dvkgpu.disableSubWorldViews=true — engine still traverses every worldview and fires every callback, we drop all of them 60-70
Entity Hider hideWorldEntities — engine skips the worldviews entirely 150+
stock GPU plugin, same spot no better

Our entire sub-worldview renderer accounts for ~15 FPS of the ~100. The rest is the engine's own per-worldview traversal, which stock pays too. Everything we ask the engine to do is identical to stock: GPU | ZBUF | NO_VERTEX_SNAPPING (ClientRuntimeConfig.java:23-29 vs GpuPlugin.java:361-364), setExpandedMapLoading default 3 on both sides, scene.setDrawDistance from config on both sides.

Testing

./gradlew build passes; shaders recompiled, spirv-val clean on both, checkGpuVulkanShadersFresh passes. The -Dvkgpu.* forwarding was verified by probing the configured run task, not by trusting the API name.

Runtime: the isolation flag has been exercised in game. The tint and the repacked push constants have not been visually confirmed yet — that needs a salvage spot with other players' ships in view, which should read as flat silhouettes.

Two problems with sub-worldview rendering, both visible while salvaging
with other players' ships around.

Other players' ships rendered in full detail. The engine puts an HSL
override on each sub-worldview's own Scene (Scene.getOverrideHue /
Saturation / Luminance / Amount); stock hands those four bytes to its
entityTint uniform (GpuPlugin.java:833,1088, VAO.java:123) and lerps in
HSL component space in vert.glsl:88, which is what turns another
player's ship into a flat silhouette. We never read them. scene.vert now
does the same lerp from a packed int, keeping stock's split where
textured faces carry the untinted lightness and untextured faces the
tinted one, so a tinted ship still shows its textures.

The scene push block was already at the 128 bytes maxPushConstantsSize
guarantees, so the tint needed room: the vertex stage takes 0..99 and
the fragment stage 100..123, with textureLightMode, colorBlindMode,
alphaMode and smoothBanding folded into one modes int. The record
signature collapses to (cmd, mvp, VulkanFrameContext, SceneEntity)
rather than growing an eighteenth parameter.

Each sub-worldview also built a whole SceneRenderer: six pipeline
compiles, a descriptor pool and set, a 12.5 MB arena and a
vkDeviceWaitIdle, all inside the draw callback, every time a ship sailed
into view. Pipeline state and descriptor contents never vary per scene,
so both move to a device-wide ScenePipelines. The arena is seeded small
and grown by the capture loop that already exists, so a rowboat does not
pay a galleon's worst case. The drain is skipped until a renderer has
recorded a draw, since nothing in flight can reference an arena it has
never drawn from. Zone frustum and overlay loops run over the captured
extent instead of the top level's 23x23 grid, which for a ship is a few
zones rather than 529. Per-scene capture logging drops to debug for
sub-worldviews.

Assisted-by: Claude Opus 5
The renderer escape hatches (disableSubWorldViews, disableFrustumCull,
fullSceneDraw, validation, modelStats) are only reachable by editing
build.gradle, because JavaExec does not inherit the gradle JVM's system
properties. Forward the vkgpu. prefix so they can be flipped per run.

Assisted-by: Claude Opus 5
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant