Repository navigation
Integrate #758, #622 and #716: upscaled textures, bounded bitmap uploads, Enhanced look - #764
Merged
Merged
Conversation
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.
The game draws two textures through their CPU pixels: the guild mark (BITMAP_GUILD, CreateGuildMark) and the castle owner's mark on the flag in Valley of Loren (BITMAP_INTERFACE_MAP, CreateCastleMark). Only a comment kept upscaled art away from them, and it named the wrong ones. An upscaled Object31/Flag.OZT lost its pixels after the upload and the castle mark wrote through a null pointer. They now load through LoadCanvasBitmap, which asks the loader to keep the pixels. The mark writers are laid out for the original size, so an upscaled file loaded into a canvas is shrunk back to that size, each pixel the average of its block, and uploaded without mips. CreateCastleMark also checks that the bitmap is loaded, like CreateGuildMark.
Apart from the two canvases, nothing reads a texture's pixels after the upload, so the loader now frees them for every texture, originals included, not only for upscaled ones. Texture Memory in the debug console counted those CPU copies, so it went down when a pack was installed and would now read almost zero. It counts the base level of each uploaded texture instead.
MAX_WIDTH and MAX_HEIGHT (1024) were checked against the stored image before the marker was read, so a 4x file of any original over 256 px, or a 2x file of one over 512 px, did not load. The log blamed TurboJPEG or an invalid OZT format. The loader now reads the marker first and accepts a marked image up to its scale times the limit; the power-of-two padding uses the same bound. A rejected image logs its size and the limit.
Resolved against the upscaled textures on main (sven-n#718) and sven-n#758: - The loader moved to GlobalBitmapLoad.cpp; it now carries sven-n#718/sven-n#758's marker scale, original-size Width/Height, premultiplied RmlUi images, canvases that keep their pixels, the released CPU copy and the base-level texture memory count. - The uploader keeps its three 4 MiB slots and stages an image larger than one slot (marked files up to 4x the 1024 px limit) in bands of rows, one submission each. - Mipmapped uploads: the GPU builds the chain after the last band, and two more shared samplers (trilinear, anisotropic, repeat/clamp) serve them, so upscaled textures no longer create a sampler each. - Tests: six shared samplers, the wider size limit, banded staging with mips, and a marked file in the loader (mipmapped, original size, released pixels; a canvas shrunk and kept).
Resolved against main's renderer and the upscaled textures (sven-n#718, sven-n#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 sven-n#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: sven-n#716's passes (frame, post-processed frame, continued flush) keep main's replay profiler scope and main's editor pass after RmlUi; sven-n#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: sven-n#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 sven-n#716's anisotropic samplers, in both looks. Opaque images get GPU-built mips, images with alpha sven-n#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.
Texture creation, mapping, acquiring and the copy pass fail before the slot is in use, so only that upload fails now. A failed submit or fence wait leaves the GPU's use of its slot unknown: that slot is taken out of service and uploads go on through the others; the uploader faults only when no slot is left. A faulted uploader keeps the error that faulted it instead of overwriting it with every rejection, and an uninitialized one says so rather than "wrong thread". From the review of sven-n#622.
…eased The static Bitmaps unloads its images again at exit, after the renderer has shut down. ReleaseFilteredSamplers then asked the renderer for its device, and GetDevice logs a warning without one, through a logger that is already destroyed: every quit crashed. Without a sampler to release it now returns before asking.
The opt-in GPU upload test now also uploads a 2048x1030 image, larger than one staging slot, with the GPU building its mips, and reads back the base level.
This was referenced Oct 17, 2026
This was referenced Oct 17, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Merges #758, #622 and #716 into main together, with their conflicts resolved once. Each PR is a merge commit here with its author's commits unchanged, and the resolution is in that merge commit. Merge this with "Create a merge commit" (not squash or rebase): GitHub then marks the three PRs as merged.
Order: #758 (follow-up to #718, merged cleanly), then #622, then #716, which contains #622 as its first commit.
Which design each area follows
Where two PRs, or a PR and main, had implemented the same thing, this table says which implementation the integration keeps and what it replaces. Each feature itself is kept.
[Render] Anisotropy[Render] AnisotropyIn short: rendering performance follows main, texture resource management follows #622 together with #758's memory handling, the visual features follow #716, and upscaled art keeps #718's behavior through #622's uploader and #716's samplers.
What the resolutions decide
#622 with the upscaled textures (#718, #758)
GlobalBitmapLoad.cpp. It now also carries feat(render): load upscaled textures marked in the file header #718's and fix(render): follow-ups to upscaled textures: canvas pixels and size limit #758's changes:docs/build/rendering-parity.mdis updated.#716 with the upscaled textures
[Render] Anisotropynow applies to every mipmapped texture: world textures in Enhanced, and upscaled ones always. Classic textures without mips are unchanged. The uploader goes back to fix(render): bound bitmap upload staging and share samplers #622's four shared samplers.#716 with the renderer changes on main
RecordSkinnedDraw, the path the GPU mesh cache (perf(render): keep skinned meshes on the GPU and upload only their colour #743) uses. feat(render): an optional Enhanced look preset beside the classic look #716's per-draw vertex upload is gone.skinned_lit.fragreads none.Fixes on top
fix(render): fail one bitmap upload on a GPU error, not the session: the failure handling above (Mosch0512's requested change on fix(render): bound bitmap upload staging and share samplers #622).fix(render): no crash at exit releasing Enhanced samplers already released: every quit crashed with feat(render): an optional Enhanced look preset beside the classic look #716's code on Windows. The staticBitmapsdestructor asked the shut-down renderer for its device, andGetDevice()logs through an already destroyed logger. This exists in feat(render): an optional Enhanced look preset beside the classic look #716 on its own.test(render): a banded upload with GPU mips reads back on a real device.Test plan
RelWithDebInfo build of every target (Windows, MSVC).
ctest: all 853 pass. Two or threerml-themed-viewUI tests fail only when run in parallel (-j8) and pass on their own; they don't touch this code.Opt-in GPU upload test (
MU_RUN_GPU_UPLOAD_TESTS=1) on Direct3D 12 and Vulkan, including a 2048×1030 banded upload with GPU mips.Classic against main, same spot in Noria, 1280×720, VSync off, Vulkan (
$glstats,$details):The FPS difference is within run-to-run variation.
In game with nolt's upscaled pack: about 1,880 mipmapped textures, about 2 GB on the GPU, no load errors. Switched to Enhanced while playing: 4× anti-aliasing, alpha-to-coverage, bloom, light mood and GPU body shadows all draw; about 275 FPS with the pack, 1.2 ms replay including the bloom pass. Switched back to Classic.
Original data: switching to Enhanced rebuilt 89 world textures as they were drawn, 12 of them with alpha (grass tiles, flowers) re-read from file into the CPU chain, with no re-read failures.
Quitting from the login screen and from the world exits cleanly.
Metal, Linux and D3D12 in game: not run here (in game was Vulkan only).
🤖 Generated with Claude Code