Skip to content

fix(render): follow-ups to upscaled textures: canvas pixels and size limit - #758

Merged
3 commits merged into
sven-n:mainfrom
nolt:fix/upscaled-canvas-and-size-limit
Oct 17, 2026
Merged

3 commits merged into
sven-n:mainfrom
nolt:fix/upscaled-canvas-and-size-limit

Conversation

@nolt

@nolt nolt commented Oct 16, 2026 •

Copy link
Copy Markdown
Contributor

Follow-up to PR #718, addressing the two follow-up points from the review.

Canvas textures keep their pixels (fix(render): keep the pixels of canvas textures)

Two textures are drawn on through their CPU pixels: BITMAP_GUILD (CreateGuildMark) and BITMAP_INTERFACE_MAP (Object31\Flag.tga, CreateCastleMark). They now load through a new LoadCanvasBitmap, which asks the loader to keep Buffer. CreateCastleMark gets the same null check as CreateGuildMark, and the comments name the right textures.

One deviation from the suggestion: the loader doesn't ignore the marker for a canvas, it shrinks the image back to the original size (each pixel is the average of its block) and uploads it without mips. Ignoring the marker would load guild.OZT at 4x as 32x32 instead of 8x8, and CreateGuildMark would then read 1024 bytes out of a 64-byte mark. Rejecting the file isn't an option either, because a failed LoadBitmap closes the game. With the shrink, a pack can include these files and nothing breaks.

Buffer is released for every texture (perf(render): release the CPU copy of every texture after upload)

Apart from the two canvases nothing reads Buffer after the upload (checked with grep over src/source), so it's freed for originals too. "Texture Memory" in the debug console counted those CPU copies; it now counts the base level of each uploaded texture, so it no longer drops when a pack is installed.

Size limit scales with the marker (fix(render): allow marked textures up to their scale times 1024 px)

The marker is read before the size check, so a marked image is accepted up to MAX_WIDTH * scale x MAX_HEIGHT * scale. The power-of-two padding uses the same bound (otherwise an image over 1024 px would be padded to less than its own size). A rejected image logs its size and the limit instead of a TurboJPEG error or "invalid format". Note that loading a 4096 px 4x image temporarily takes about 160 MB in the 32-bit client; the current pack has none.

Testing

  • Built on Linux (Ubuntu 26.04 image): the changed files compile, and upscale_marker_tests and ozt_file_tests pass (17/17), including new tests for the shrink and for reading the marker's scale.
  • Tested in game on Linux with the 4x pack installed, on top of current main + PR 718: world and UI textures, guild marks, Valley of Loren (castle flag loads; no castle owner, so the mark itself wasn't drawn), general play. Resident memory on the same map: 2,078,644 KB with main + PR 718, 659,548 KB with this PR on top (about 1.4 GB less). No commit between the two main bases touches texture loading.

nolt added 3 commits October 16, 2026 22:03
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.
@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 #716, whose conflicts with #718 and #758 are 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.

What #764 does with this PR's changes:

I tested it in game with your pack: about 1,880 textures mipmapped, about 2 GB on the GPU, no load errors, in both Classic and Enhanced.

@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 the upscaled textures on main (#718) and #758:

- The loader moved to GlobalBitmapLoad.cpp; it now carries #718/#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).
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.
@nolt
nolt deleted the fix/upscaled-canvas-and-size-limit branch October 17, 2026 16:30
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.

2 participants