Skip to content

feat(render): an optional Enhanced look preset beside the classic look - #716

Merged
4 commits merged into
sven-n:mainfrom
alandarev:feature/enhanced-look
Oct 17, 2026
Merged

4 commits merged into
sven-n:mainfrom
alandarev:feature/enhanced-look

Conversation

@alandarev

@alandarev alandarev commented Oct 13, 2026 •

Copy link
Copy Markdown
Contributor

Summary

Adds Enhanced, an optional look beside the classic one. Classic stays the
default and draws exactly as before
; a player switches with the "Enhanced
graphics" checkbox on the options window's Video tab ([Render] Look=Classic|Enhanced, or
$look classic|enhanced in the chat). Closes #715.

Enhanced is a preset: one switch for six pieces, each of which is tuned in
data or settings, not code:

Piece What it does Tuned in
Texture filtering coverage-preserving mip chains and anisotropic sampling for world textures (terrain, objects, models), so distant ground stops shimmering [Render] Anisotropy
Enhanced lighting per-pixel model lighting and fog: wrapped diffuse, rim light, highlight roll-off, specular on metal armour lighting.* in Data/Render/Look.json; material.specular per item model
Dynamic model lights the lights the game already adds to the terrain (candles, fires, spells) light characters and monsters too, through a per-frame light history with fade, range and strength limits dynamic.* in Data/Render/Look.json
Light mood per-map deeper shade, sun direction and model brightness on top of the baked terrain light Data/Maps/LightMood.json
Post-processing selective bloom (bright effects, not the UI) and per-map colour grading tables bloom.*, grading.* in Data/Render/Look.json; Data/Grading/
Anti-aliasing multisampled edges, alpha-to-coverage for cut-out foliage and fences [Render] Antialiasing (samples, default 4; $msaa off|2|4|8)

Every value can be set for the whole game or per map. Developer builds (control
socket or editor) change any value live and save it back ($look set <name> <value>, $look save, a Look panel in the DevEditor), and switch one piece for
the session ($texfilter, $lighting, $dynlights, $lightmood, $post), so
a server or a player can build their own Enhanced without touching the code.
docs/enhanced-look.md has the full map. Classic never uses anti-aliasing,
whatever Antialiasing says, so it stays the game's own look.

Media (Classic left, Enhanced right; the game's default camera):

Lorencia tavern
Fight by a bonfire
Dungeon, Aqua Beam and Cometfall
Noria

More stills and a 32 s clip that flips between the looks:
https://github.com/alandarev/mumain-sven/tree/media/enhanced-look

Depends on #622

The first commit is #622 (bounded bitmap upload staging and shared samplers),
rebased onto current main (112cece, after #610); #622 is updated to the
same commit. The rebase adapts it to OztFile (its own OZT header check is
replaced by Ozt::ReadHeader) and to ErrorDialog, and moves the editor's
load-world bookkeeping and #610's LoadImageExclusive, padToPowerOfTwo and
CreateBitmapStorage into the loader file #622 introduced. The mip chains upload
through its fixed staging slots: BitmapGpuResources::Upload takes the levels
below the image and stages as many consecutive levels per 4 MiB slot as fit, so a
full 1024×1024 chain takes two submissions and no slot grows. If #622 is merged
first this PR shrinks to its own three commits.

Commits

  1. fix(render): bound bitmap upload staging and share samplers — fix(render): bound bitmap upload staging and share samplers #622.
  2. feat(render): an Enhanced look preset beside the classic look — the code:
    Render/Look/ (profile, preset, live tuning), Render/Lighting/ (dynamic lights,
    light history, light mood), Render/PostProcessing/, Render/Textures/
    (mip chains, filtering), renderer and shader changes, the options checkbox
    (a row on the RmlUi option window's Video tab, both themes), config keys,
    tests. Shaders are regenerated for SPIR-V, MSL and DXIL with the
    project's commands.
  3. feat(render): the tuned values of the Enhanced look — Data/Render/Look.json,
    Data/Maps/LightMood.json, Data/Grading/World2.png, material.specular on the
    metal armour sets, and tests that pin them.
  4. docs: … — docs/enhanced-look.md, light-mood.md, post-processing.md,
    options window.

203 files, +19.6k/−1.1k; without shader blobs and data +18.4k/−1.0k, of which a
large part is tests (28 test files).

Rebased onto #610 (RmlUi)

  • The checkbox moved from the deleted native options window to the RmlUi
    option window (Video tab); the native combo box and layout changes are gone.
  • Multisampling covers the frame's main pass. feat: rmlui UI system #610 draws more passes into
    single-sample targets (RmlUi render targets, the post-RmlUi pass, offscreen
    captures); while anti-aliasing is on they replay through single-sample twins
    of the multisampled pipelines, built and released with them (Classic builds
    none). Models drawn into RmlUi render targets keep Classic texture sampling.

Try it

cmake --preset linux-x64 && cmake --build --preset linux-x64-release
# start the client, Options > tick "Enhanced graphics" (or type $look enhanced)

Testing

main (112cece) does not build on Linux (#730). The Linux runs below
therefore add the Linux build fixes of #719 (fork branch fix/linux-gcc-build
9ecbf65) under this branch, test-only; they are not part of this PR.

  • cmake --build --preset linux-x64-release + ctest: each commit builds and
    passes on its own: 621 / 814 / 817 / 817.
  • linux-x64-mueditor release build + ctest: 855/855, except
    bundled_font_role_tests, which does not link with the editor on main
    either (Build fails on main (112cece4d): macOS, Windows/Linux with the editor, Linux; CI has not run on main since August #730 §4).
  • CI on the fork for this head, merged with main 2e41b7c: Quality Gates and the
    Windows editor-OFF build with its tests pass (https://github.com/alandarev/mumain-sven/actions/runs/38795377375,
    https://github.com/alandarev/mumain-sven/actions/runs/38795377425). The other jobs fail on main too, with the same errors
    (Build fails on main (112cece4d): macOS, Windows/Linux with the editor, Linux; CI has not run on main since August #730): Linux strncpy_s, macOS WSclient.cpp:10828 narrowing, and on
    Windows with the editor bundled_font_role_tests, which does not link
    (MouseX/MouseY twice and 40 unresolved Winmain.cpp symbols on main;
    this branch adds three more of the same kind: MuSetAntialiasingActive,
    MuSetAntialiasingPreference, MuSetTextureFilteringEnabled).
  • clang-format 21.1.8 on the changed ranges and Cppcheck with the CI options:
    no findings, on every commit.
  • GPU upload test with the Vulkan validation layer
    (MU_RUN_GPU_UPLOAD_TESTS=1 MU_GPU_UPLOAD_VALIDATION=1): a 1024×1024 chain (11
    levels) through the fixed slots reads back exactly at every level.
  • Classic is unchanged: this branch and main (both with fix(linux) build/portability crashes #719's fixes) were
    each run twice (main, branch, main, branch) with a fresh configuration on 14
    spots across the maps. On the 10 spots with enough still content to score,
    over the 8×8 windows that are static in all four runs, the SSIM between the
    two builds is at or above the SSIM between two runs of the same build on
    every spot; on 9 of them 96–100 % of those windows are bit-identical between
    the builds (Devias moves too much between runs to compare windows exactly; two
    runs of the same build differ there as much). Classic creates none of the
    single-sample pipelines above.
  • Enhanced, compared with Classic at 2560×1440 with a character in 8 scenes
    (towns, dungeons, fights), measured before the rebase onto feat: rmlui UI system #610: characters
    no shinier in 7 and at least as readable against the ground in 6. The
    exceptions: in the fight next to a bonfire the fire brightens the characters
    (top 5 % of their pixels 253 vs 250 of 255) and the ground around them; in
    the tavern, anti-aliasing softens the silhouette edges slightly
    (figure-to-ground 0.99 vs 1.01; 1.007 without it). Spells light nearby
    characters at least as much as in Classic (brightness rise 14.8 vs 13.0).
  • Frame time at 2560×1440 on an RTX 5090, fight by the bonfire, VSync off
    (before the rebase): Classic median 2.49 ms (p95 3.45), Enhanced with 4x
    anti-aliasing 2.79 ms (p95 4.21).
  • Self-review: Claude Code's /code-review over the whole diff (49 passes)
    found 9 points; 3 fixed in this branch (a stale comment, a look profile
    whose existence cannot be checked is now reported unreadable, a lingering
    light that finds the light list full no longer evicts a stronger one), 6
    were not defects.

Open points

  • Only Linux/Vulkan was run. The Metal and Direct3D 12 shaders are generated by
    the project's commands and pass the reflection checks, but have not been run.
  • Up to 32 point lights are lit on models per frame, chosen nearest the hero; in
    a crowded fight some distant lights are dropped.
  • Enhanced is off by default on purpose; making it the default is your call.

Written with AI assistance and tested by hand.

Checklist

  • I have read and followed docs/CODING_RULES.md.
  • I have built the project locally (see docs/build/README.md).
  • Changes are focused on one concern; the diff is as small as it reasonably can be. — one concern (the Enhanced look), but large; split into four commits.

Use three persistent fenced staging buffers and four device-owned bitmap samplers instead of per-image allocations. Preserve whole-image uploads and existing decoder limits, check submission failures, and keep borrowed sampler ownership explicit.

Add decoded-loader, failure/cleanup, truncated OZT and non-presenting GPU regression coverage, including intermediate readbacks and opt-in Vulkan validation.

Refs sven-n#620
Adds a second look the player can switch to, while the classic look stays
the default and draws exactly as before. `[Render] Look=Classic|Enhanced`
(the "Enhanced graphics" checkbox in the options window, `$look classic|
enhanced` in the chat) switches five features and anti-aliasing on or off
together:

- texture filtering: coverage-preserving RGBA8 mip chains and anisotropic
  sampling for world textures (terrain, objects, models), built on load or
  on first use; the chains upload through the fixed staging slots of the
  bitmap owner, as many levels per slot as fit;
- per-pixel model lighting and fog (skinned_lit, basic_textured_fog):
  wrapped diffuse, rim and per-item specular (`material.specular` in the
  item-model JSON), a highlight roll-off that keeps burnt-out tops shaded;
- dynamic model lights: the lights the game already adds to the terrain
  (candles, fires, spells) also light characters and monsters, through a
  per-frame light history with fade, range and strength limits;
- a per-map light mood: deeper shade, a sun direction and model brightness
  per map, scaled from the baked terrain light;
- scene post-processing: a selective bloom (bright effects, not the UI) and
  per-map colour grading tables;
- multisample anti-aliasing, with alpha-to-coverage for alpha-tested cutouts,
  at `[Render] Antialiasing` samples (default 4, `$msaa off|2|4|8`); Classic
  always renders one sample per pixel.

Every value of the preset lives in two data files with per-map overrides
(`Data/Render/Look.json`, `Data/Maps/LightMood.json`); without them the
built-in values apply. Builds with the control socket or the editor tune
any value live (`$look set <name> <value>`, `$look save`), switch one
feature for the session (`$texfilter`, `$lighting`, `$dynlights`,
`$lightmood`, `$post`) and the DevEditor gets a Look panel. The anisotropy
level (`[Render] Anisotropy`) is a setting of its own.

With RmlUi: the checkbox is a row on the options window's Video tab (both
themes; styled for `legacy`). Multisampling covers the frame's main pass
only; the passes that render into single-sample targets (offscreen
captures, RmlUi render targets, the post-RmlUi pass, flushed command
ranges) replay with single-sample twins of the multisampled pipelines,
built and released with them. Models drawn into RmlUi render targets
(item previews, the character-creation preview) keep Classic sampling.

Shaders are regenerated for all three backends with the project's
commands; Vulkan is tested, Metal and Direct3D 12 compile but have not
been run.
The Enhanced preset's values, chosen by comparing candidates side by side
in towns, dungeons and fights across the Season 6 maps:

- `Data/Render/Look.json`: model lighting (diffuse, rim, specular, wrap,
  highlight roll-off), dynamic light gain, range and caps for effect
  lights, bloom and grading; Icarus keeps a weaker bloom for its clouds;
- `Data/Maps/LightMood.json`: the default mood and per-map moods (sun,
  shade, brightness) so that dark maps stay readable;
- `Data/Grading/World2.png`: Dungeon's colour grading table;
- `material.specular` 0.6 on the metal armour sets of the Dark Knight,
  Magic Gladiator and Dark Lord (115 parts); cloth-heavy sets stay matte,
  since specular applies to the whole model.

Tests pin the shipped values and the list of shiny armour parts.
`docs/enhanced-look.md` describes the two looks, the settings, which file
holds each piece of the Enhanced preset (model lighting, dynamic lights,
bloom and grading, light mood, specular armour), per-map overrides, and the
developer commands that tune values live and save them. `light-mood.md` and
`post-processing.md` go deeper into those two pieces; the options window
page names the "Enhanced graphics" checkbox.
@nitoygo

nitoygo commented Oct 17, 2026

Copy link
Copy Markdown
Collaborator

This PR is merged unchanged into the integration PR #764, together with #622 and #758, with the conflicts against main's renderer changes resolved there. When #764 merges with a merge commit, GitHub marks this PR as merged too.

Please don't push to this branch until #764 merges. A new commit here would leave this PR open, and it would have to be merged into #764 again.

The main changes to how this PR fits in (#764's description has the full list):

  • Skinned lighting. Enhanced lighting moved into RecordSkinnedDraw, the path of the GPU mesh cache (perf(render): keep skinned meshes on the GPU and upload only their colour #743). The per-draw vertex upload is gone.
  • Frame replay. Your multi-pass replay is kept, with main's profiler scope and main's editor pass after RmlUi.
  • Body shadows. The GPU body-shadow pipelines (perf(render): draw body shadows on the GPU #749) are part of the single-sample pipelines used when anti-aliasing is on.
  • Textures.
    • Upscaled textures use your shared anisotropic samplers in both looks, so [Render] Anisotropy applies to every mipmapped texture. docs/enhanced-look.md is updated.
    • Opaque textures get GPU-built mips; textures with alpha keep your alpha-preserving chain.
    • fix(render): follow-ups to upscaled textures: canvas pixels and size limit #758 releases the pixels after upload, so switching to Enhanced re-reads the file. In game, 89 textures got their mips this way after the switch, 12 of them alpha textures, with no failures.
  • A bug in this PR: every quit crashed on Windows. At exit, the static Bitmaps destructor calls ReleaseFilteredSamplers(), which asks the already shut-down renderer for its device, and GetDevice() logs through a logger that has been destroyed. Fixed in c7acc14.

Classic is unchanged against main (same spot in Noria: same draws, 0.55 vs 0.54 ms replay). I tested Enhanced on Vulkan with 4× anti-aliasing, bloom and the upscaled pack.

@nitoygo nitoygo closed this pull request by merging all changes into sven-n:main in 8a5934c Oct 17, 2026
nitoygo added a commit that referenced this pull request Oct 17, 2026
Resolved against main's renderer and the upscaled textures (#718, #758):

- Skinned draws: Enhanced lighting (its pipelines, alpha-to-coverage, the
  world sampler, eye position, specular and world-space rows) moved into
  RecordSkinnedDraw, the path of the GPU mesh cache; the per-draw vertex
  upload of #716's old path is gone. A skinned draw's colour is one
  per-instance value, so it gives the draw's alpha.
- Frame replay: #716's passes (frame, post-processed frame, continued
  flush) keep main's replay profiler scope and main's editor pass after
  RmlUi; #716's editor marker inside the replay is dropped.
- The GPU body-shadow pipelines join the single-sample twins, so shadows
  still draw outside the multisampled main pass; they take no coverage.
- Upload: #716's mip levels and the row bands share one staging loop; a
  texture can also have the GPU build its chain. The uploader is back to
  four shared samplers.
- Textures: upscaled art is always mipmapped and sampled through #716's
  anisotropic samplers, in both looks. Opaque images get GPU-built mips,
  images with alpha #716's coverage-preserving CPU chain, at load and on
  the switch to Enhanced. The pixels are released after the upload, so
  that switch reads the file again.
- Shader checks: both sides' shaders in the validation lists; the raw
  bone buffer check covers the skinned vertex shaders only.
- Docs: the upload staging note, and Anisotropy applies to every
  mipmapped texture.
@alandarev
alandarev deleted the feature/enhanced-look branch October 17, 2026 10:43
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.

An optional "Enhanced graphics" look beside the classic one

2 participants