Commit Graph
10000 Commits
Author SHA1 Message Date
Henrik RydgårdandClaude Opus 5.5 2d3c8fa0cb SoftGPU: Apply UV scale and offset like the GE
In UV gen mode 0 the software renderer now decodes UVs without the prescale
and applies u * scale + offset itself: the product as a float24, then the
GE's guard-bit-less add. Bit exact on gpu/probe exp66 for float, 16-bit and
8-bit UVs (which are unsigned).

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-02 15:09:42 -06:00
Henrik RydgårdandClaude Opus 5.5 abd4c21b9b SoftGPU: Texture matrix and projected UVs like the GE
The texture matrix is summed like a position row, and projected texture
coordinates go through the same s, t, q planes as perspective ones, with q
the texture matrix's q over w. Points use the planes too. Bit exact on
gpu/probe exp64-65, except the normalized normal source, whose normalization
isn't matched.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-02 15:09:42 -06:00
Henrik RydgårdandClaude Opus 5.5 92e813b53e SoftGPU: Scale specular and attenuated light like diffuse
Specular uses the same 8-bit factor expansion as diffuse, and attenuation
(with the spot factor) is a second 8-bit factor applied after it, also to
ambient. Exact on gpu/probe exp63 for specular, 99.5% for point lights (the
attenuation's own precision is not matched).

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-02 15:09:42 -06:00
Henrik RydgårdandClaude Opus 5.5 eb5fad4e10 SoftGPU: Scale diffuse light like the GE
The light and material colors first make an 8-bit product, and N.L becomes an
8-bit s = floor(256 * f), which is expanded like a color before the second
multiply: ((2x + 1) * (2s + 1)) >> 10. Bit exact on gpu/probe exp61-62, apart
from N.L itself being about 2^-15 high on the PSP with a scaled world matrix.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-02 15:09:42 -06:00
Henrik RydgårdandClaude Opus 5.5 a75df22ddc SoftGPU: Select mip levels in slope mode like the GE
Per pixel, from the interpolated q = 1/w: 16 + log2(slope) - log2(q), both
with the same float-bits log2, plus the bias. Bit exact on gpu/probe
exp58-60, including nearest level rounding.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-02 15:09:42 -06:00
Henrik RydgårdandClaude Opus 5.5 15467e24c4 SoftGPU: Sprite UVs and texel addressing like the GE
Sprites interpolate UVs with the same fixed point planes as triangles, and
texture coordinates become texels at 1/16 texel, truncated toward zero, also
in the sampler JIT. Bit exact on gpu/probe exp57 (through mode and transform
mode sprites, nearest and bilinear).

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-02 15:09:42 -06:00
Henrik RydgårdandClaude Opus 5.5 0f2b5613fb SoftGPU: Interpolate Gouraud color and perspective UVs like the GE
Both use the GE's fixed point depth plane. Color is screen-linear, also in
transform mode, with 8-bit channels as the plane values (gpu/probe exp54). For
perspective UVs, s = u * R(w), t and q = R(w) become 15-bit integers at the
triangle's largest exponent, go through the plane, and u = s * R(q) per pixel
(exp55-56, bit exact at 1/16 texel).

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-02 15:09:42 -06:00
Henrik RydgårdandClaude Opus 5.5 f651a0abf8 SoftGPU: Don't skip color doubling in the sprite fast path
A white vertex color still gets doubled, and so does replace. Found with
gpu/probe exp53.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-02 15:09:42 -06:00
Henrik RydgårdandClaude Opus 5.5 7d3131abc2 SoftGPU: Bilinear filter horizontally first, truncating, like the GE
The GE lerps the top and bottom texel pairs, truncates each to 8 bits, then lerps
those vertically. Bit exact on gpu/probe exp52 (also in ppsspp-re tools/geprobe).

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-02 15:09:42 -06:00
Henrik RydgårdandClaude Opus 5.5 524fac1c32 SoftGPU: Clip at the near plane like the GE
Measured with gpu/probe exp43-47, bit exact:
- New vertices are interpolated from the inside vertex, t = d_in / (d_in -
  d_out) with d = z + w through the GE adder, using the GE's reciprocal;
  out - in is truncated to float24 before the multiply by t, and t and each
  coordinate use float24 products and adds.
- With one vertex outside, the quad is split from the vertex before it in the
  GE's order: (p, a, b) and (p, b, n). The GE takes odd strip triangles in
  reverse and fan triangles in order, so SendTriangle now says when the
  vertices it gets are reversed from that.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-02 15:09:42 -06:00
Henrik RydgårdandClaude Opus 5.5 3c450eed4b Headless: Add --depth-save and --replay-end for frame dump comparisons
--depth-save writes the depth buffer next to the screenshot (u32 width, u32
height, then u16 depths), and --replay-end stops a frame dump replay after
that many primitives. Both match pspautotests' ppdmp-playback, so a dump's
depth can be compared with the PSP's at any point in the frame.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-02 15:09:42 -06:00
Henrik RydgårdandClaude Opus 5.5 df8f1663dd SoftGPU: Interpolate depth with the GE's fixed point plane
Bit exact against the PSP on every pixel of gpu/probe exp36-41 (over a million
pixels of large, small, steep and slanted triangles):
- Per triangle, the doubled area's 16-bit significand indexes a 256-segment
  reciprocal table, linear over each segment's low 8 bits (measured for all
  inputs; not the z/w table).
- The x and y depth gradients are fixed point, 14 fractional bits per
  subpixel, from the exact edge cross products times that reciprocal.
- The plane is anchored at the leftmost vertex, or the rightmost when the long
  edge (top to bottom) is strictly the right side of the triangle.
- Each pixel takes the plane at its center, floored.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-02 15:09:42 -06:00
Henrik RydgårdandClaude Opus 5.5 913777f948 SoftGPU: Sum matrix rows and combine matrices like the GE
Measured with gpu/probe (exp30, exp32, exp34, exp35, exp42), bit exact on
all of it (51911 points through general world, view and perspective
matrices, position and depth):
- The vertex position is truncated to a float24.
- Each product in a matrix row keeps the exact value truncated at a fixed
  bit weight, 2^-15 below the two inputs' exponents combined, so a
  significand product of 2 or more keeps a 17th bit.
- A row is one multi-operand sum: every product and the translation are
  truncated to the bit weight of the term with the largest exponent, added
  exactly, and the result becomes a float24. There's no order.
- World, view and projection are combined as (world * view) * projection,
  each entry summed the same way and kept as a float24.

The Coded Arms PSP replay gets worse (MSE 26 to 52). Its decals depend on
per-pixel depth, whose interpolation hasn't been measured yet.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-02 15:09:42 -06:00
Henrik RydgårdandClaude Opus 5.5 70d138804f SoftGPU: Take the 1:1 sprite fast path's texels at pixel centers
The GE samples a pixel's texture coordinates at its center, so a 1:1 sprite
starting at u0 shows texel floor(u0 + 0.5) first (floor(u0 - 0.5) when
mirrored). The fast path truncated u0, half a texel off for fractional UVs.
Measured with gpu/probe; integer UVs are unaffected.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-02 15:09:42 -06:00
Henrik RydgårdandClaude Opus 5.5 3bf25586bd SoftGPU: Compute X and Y like the GE, and sample at pixel centers
X and Y now go through the same float24 math as Z: clip coordinates from the
combined matrix (also when lighting needs the world position), the GE's
reciprocal, and the viewport offset added without guard bits, which around
2048 lands on the 1/16 subpixel grid. The 12.4 conversion truncates instead of
rounding up at 10/16.

Triangles are sampled exactly at pixel centers, not 1/16 before them, with left
and top edges inclusive. Sprites start at (x + 6) >> 4 horizontally and
(y + 7) >> 4 vertically and end at (v + 7) >> 4, and their 2x2 loop no longer
treats ties differently by pixel parity. Measured with gpu/probe; against PSP
replays, Coded Arms goes from MSE 33 to 26 and the Test Drive map from 17 to 7.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-02 15:09:42 -06:00
Henrik RydgårdandClaude Opus 5.5 3d4f945054 SoftGPU: Clip and cull at the near plane like the GE
With depth clip off, the GE doesn't clip triangles and lines at the near
plane: the part beyond it is drawn with extrapolated depth, and a vertex behind
the camera (w <= 0) culls the whole primitive instead. With it on, they're
clipped, and a vertex that gets clipped away can't cull the primitive by being
outside the screen range (w = 0 included). Rects, which aren't clipped, are
culled by a vertex behind the camera either way. Measured with gpu/probe.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-02 15:09:42 -06:00
Henrik RydgårdandClaude Opus 5.5 f48251c90a SoftGPU: Cull primitives by z like the GE
The GE counts a vertex as outside when |z| > w, comparing the float24 clip
coordinates without dividing, and culls a primitive only when all its vertices
are outside, with depth clamp on or off. Points were never culled, and lines,
triangles and rectangles were culled on a single outside vertex with clamp off.
Measured with gpu/probe for every primitive type, both clamp modes and a
range of w.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-02 15:09:42 -06:00
Henrik RydgårdandClaude Opus 5.5 c35f7527a3 SoftGPU: Quantize fog per vertex, like the GE
The GE converts the fog factor to 8 bits per vertex, min(floor(256 * f), 255),
and interpolates that linearly in screen space. Clamping after interpolation
instead was up to 127 levels off on triangles that cross the fog range.
Measured with gpu/probe; points and wacky values still match exactly.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-02 15:09:42 -06:00
Henrik RydgårdandClaude Opus 5.5 6106280707 SoftGPU: Do the GE's float24 adds with bit masks
Truncating both terms to the precision of the larger one is clearing the
low 8 + (exponent difference) bits of each, after which a float sum is
exact. Only the products still go through doubles, since a product of two
float24s needs up to 32 bits before it's truncated. Same results.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-02 15:09:42 -06:00
Henrik RydgårdandClaude Opus 5.5 64fce1bb3f SoftGPU: Compute depth with the GE's precision
Measured on hardware (gpu/depth/transformprecision, gpu/depth/precision):
clip Z and W come out of the combined matrix as float24s, z/w uses the
GE's reciprocal, which linearly interpolates 128 table segments and isn't
quite 1/w, and the viewport's z * scale + center adds without guard bits:
the smaller term is truncated to the precision of the larger one.

Fixes the Test Drive Unlimited map (#12786) and the missing background in
Fuuun Shinsengumi Bakumatsuden (from #16131), where vertices land right at
the minimum depth.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-02 15:09:42 -06:00
Henrik RydgårdandClaude Opus 5.5 192dc5b345 SoftGPU: Fix black screenshots with the DarkStalkers present hack
The hack skips the game's stretch blit, so the display framebuffer stays
black and savestate thumbnails and screenshots came out empty. Do the
stretch from the source image when reading back the display framebuffer.

AGENTS.md: Commit finished work without asking, push only when asked

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-01 12:57:36 -06:00
sum2012 6ccab3d1d0 Add Worms to compat by Gemini
Fix #18713.
Fix assert
2026-10-01 11:28:57 +08:00
Henrik RydgårdandClaude Opus 5.5 333035df03 Lighting: Compute the GE's pow from the float's bits
Read as an integer, a float's bits are its log2 with the mantissa a
straight line between powers of two, scaled by 2^23, and writing an
integer back is the matching exp2. That's exactly the GE's
approximation, without log2/exp2/floor. pspPow now also returns 1 for
e <= 0 itself, so the callers drop their checks. Shader languages
without integers fall back to a true pow.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-09-30 15:47:16 -06:00
Henrik RydgårdandClaude Opus 5.5 9162592493 SoftGPU: Add the secondary color on the portable triangle path
Without SSE or NEON, triangle pixels got the secondary color in place of
the primary one plus it, so lit triangles came out black. It showed as
the "unexplained" known failures on riscv64 and loongarch64, and broke
the new gpu/lighting/shademap there. Reproduced on arm64 by building
without NEON.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-09-30 15:47:16 -06:00
Henrik RydgårdandClaude Opus 5.5 a6849661ba Shade mapping: Use the light vector as lighting sees it
Environment map S and T are (N.L + 1) / 2 with L the light's vector as
lighting uses it: from the vertex to the light for point and spot lights,
a zero vector staying zero, and the half vector for a light that does
specular. Whether lighting or the light is enabled still doesn't matter
(gpu/lighting/shademap).

The vertex shader ID now carries the type and computation of the shade
mapping lights (the ubershader reads them from u_lightControl), so both
shader caches get a new version.

Fixes the hair shine in iDOLM@STER SP (#12376).

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-09-30 15:47:16 -06:00
Henrik RydgårdandClaude Opus 5.5 6f6d3e2a81 Lighting: Take the specular viewer direction from the view matrix
The viewer is at infinity along view space +z, so in world space, where
lighting happens, it's the view matrix's third column rather than
(0,0,1): turning the camera moves the highlights (gpu/lighting/specular).
The shaders read it from u_view.

In a Need for Speed Carbon frame replayed on a PSP, this and the pow
bring the error of the cars from MSE 957 to 120 (Vulkan).

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-09-30 15:47:16 -06:00
Henrik RydgårdandClaude Opus 5.5 48f3d5c907 Lighter: No specular from a light behind the surface
The shaders and the software renderer only add specular when N.L >= 0,
as the GE does (gpu/lighting/specular); the CPU lighter, used for points,
lines and rectangles, didn't check.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-09-30 15:47:16 -06:00
Henrik RydgårdandClaude Opus 5.5 d7a96875fb Lighting: Use the GE's approximate pow for specular, diffuse and spot
The GE computes these powers as exp2(e * log2(x)), with log2 and exp2
each a straight line between powers of two (Mitchell's approximation),
and only uses the top 4 bits of the specular coefficient's mantissa.
Through a highlight's falloff a true pow is 10-30 steps of 255 brighter
at the exponents games use. Measured in gpu/lighting/specular.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-09-30 15:47:16 -06:00
Henrik Rydgård a7ec706a4c Merge pull request #22400 from hrydgard/pacman-spline-lighting
Spline lighting fixes
2026-09-30 15:46:20 -06:00
Henrik RydgårdandClaude Opus 5.5 0715f43ab8 Splines: Detect poles relative to the other derivative
With animated control points the pole is only nearly degenerate, so the
vanishing derivative is rounding noise rather than exactly zero, and the
absolute threshold missed it. The resulting random normals still showed
as dark patches on Pac-Man Arrangement's ghosts (#12354).

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-09-30 13:20:01 -06:00
Henrik RydgårdandClaude Opus 5.5 aa38fafd77 DrawEngine: Don't drop large batches of points, lines and rectangles
Software transform expands each point, line and rectangle to four
vertices, and gave up on the whole draw when that didn't fit
VERTEX_BUFFER_MAX. Batching only counted input vertices, so a batch over
16384 points (or 32768 line or rectangle vertices) vanished silently,
whether it came from one PRIM or several merged ones. Count the expanded
vertices when batching, and submit a PRIM too big on its own in parts.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-09-30 12:04:53 -06:00
Henrik RydgårdandClaude Opus 5.5 22c2cea923 Splines: Fix a single patch with both edges open
With only one patch, the open-last-edge adjustment assumed the first edge
was closed, so one knot interval came out as 2 instead of 1, and the
patch wasn't the Bezier patch a fully clamped cubic is. Positions were off
by up to about 7% of the patch.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-09-30 11:11:23 -06:00
Henrik RydgårdandClaude Opus 5.5 7a06e25aa0 GPU: Remove leftovers from hardware tessellation
The GLES sampler uniforms and texture slots for the control points and
weights, the Vulkan storage buffer bindings, and the u_spline_counts
uniform, which becomes padding (the C++ side already was).

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-09-30 11:00:02 -06:00
Henrik RydgårdandClaude Opus 5.5 f87a07b1a3 Splines: Use the limit normal at a pole instead of NaN
Where all the control points along a patch edge meet at one point, like
the top of a dome, one derivative is zero and so is the cross product,
and normalizing it gave NaN. Use the limit instead, built from the mixed
second derivative. Fixes the dark spots on the ghosts' heads in Pac-Man
Arrangement (#12354).

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-09-30 10:57:05 -06:00
Henrik Rydgård ae460c9e1a Merge pull request #22390 from hrydgard/gpu-review-leftovers
Claude code review of GPU: Fix leftover findings
2026-09-29 15:43:31 -06:00
Henrik RydgårdandClaude Opus 5.5 b9501df529 TextureReplacer: Fix stale and shared lookup results
- Reloading the ini clears the per-key lookup caches, which could keep
  saying "no replacement" for textures the new ini replaces.
- With ignoreAddress, hash ranges were skipped when sizing the
  replacement, though ComputeHash applies them. cache_ is now keyed by
  the full key, so its lookups hit too.
- Textures sharing files but differing in size, hash range or filtering
  no longer share one ReplacedTexture (the first one's settings won).

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-09-29 13:28:21 -06:00
Henrik RydgårdandClaude Opus 5.5 b8789af997 GPU: Draw frames displayed from RAM in non-buffered mode
They were uploaded but never drawn, and the block transfer hack drew
whatever source was left over. Draw them straight into the backbuffer
pass, without post shaders, which would need to bind their own targets.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-09-29 13:17:01 -06:00
Henrik RydgårdandClaude Opus 5.5 7e553b0732 GE debugger: Lock when adding command breakpoints
Debuggers add them from their own threads, racing ClearTempBreakpoints
on the emu thread.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-09-29 13:17:00 -06:00
Henrik RydgårdandClaude Opus 5.5 035c98d14f D3D11: Re-read the device and context on DeviceRestore
The restored draw context can be a new device, so the cached pointers
could go stale.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-09-29 13:16:59 -06:00
Henrik RydgårdandClaude Opus 5.5 ba37f327b0 D3D11: Check Map() results
Map fails after device removal, leaving pData garbage. Skip the upload or
draw instead of writing through it.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-09-29 13:16:59 -06:00
Henrik RydgårdandClaude Opus 5.5 bc581349fd GPU: Install the draw engines' invalidation callback from BeginFrame
The draw engine is created on the loader thread while the UI thread may
already be rendering and invoking the callback.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-09-29 13:16:42 -06:00
Henrik RydgårdandClaude Opus 5.5 759b494b6a GPU: Check post shaders once per host frame, after resizes
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-09-29 13:16:41 -06:00
Henrik RydgårdandClaude Opus 5.5 4749872f2e Vulkan: Clear a texture level when hardware scaling fails
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-09-29 13:16:41 -06:00
Henrik RydgårdandClaude Opus 5.5 4c2086aac5 GPU: Keep spline/bezier tessellation from reaching zero
Reduce only the larger factor when over the vertex limit, and stop at 1.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-09-29 13:16:40 -06:00
Henrik RydgårdandClaude Opus 5.5 9d3022642b SoftGPU: Fix bin queue bookkeeping and dependency tracking
- Flush() on an empty queue now trims the state and CLUT rings, since its
  callers flush because one is full and push right after.
- BinQueue::Full() uses >=, so an overshoot can't go unnoticed.
- IsExactSelfRender compares against the target the queued draws were
  binned for, not gstate, which already has the next one during a flush.
- A depth test without depth writes marks the depth buffer as read.
- The DarkStalkers untextured sprite recomputes the binner state around it.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-09-29 13:16:40 -06:00
Henrik RydgårdandClaude Opus 5.5 7de7b2d76b TextureCache: Use XXH3 instead of the quick hash for texture data
With xxhash 0.8.4, XXH3 is faster than StableQuickTexHash on ARM64
(about 37 vs 24 GB/s on a Snapdragon X), and its scalar path, which
RISC-V builds get, is about as fast as the quick hash's. It doesn't
collide the way the quick hash does (#8249).

Texture replacement still uses its own hash setting, so texture packs
are unaffected.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-09-29 13:10:28 -06:00
Henrik RydgårdandClaude Opus 5.5 74553bfa1a D3D11: Note that the state object caches are deliberately never trimmed
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-09-29 12:43:12 -06:00
Henrik RydgårdandClaude Opus 5.5 93f57a4f38 GPU: Delete copy operations on classes that own resources
These own GPU objects, memory or refcounts in their destructors (or assert
there that they were torn down), so a copy would double-free. Nothing copies
them today; this keeps it that way. The manager base classes cover every
backend's subclass.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-09-29 10:33:32 -06:00
Henrik RydgårdandClaude Opus 5.5 39049a67fd GLES: Free textures on device lost, and unsubmitted step data at exit
- The texture and fragment test caches dropped their GLRTexture objects on
  DeviceLost without queueing them for deletion, leaking them on every
  Android background/resume. The deleter already skips the GL calls when
  the context is gone.
- GLRenderManager::ThreadEnd cleared unsubmitted init and render steps
  without freeing the data they own. Run them through the dry run instead,
  which now also frees stereo matrices and shader code.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-09-29 10:33:32 -06:00
Henrik RydgårdandClaude Opus 5.5 b72927bbeb Texture replacement: Plug leaks on error paths
- Release the file reference when loading a level fails or finds nothing,
  since only a loaded level takes ownership of it.
- Free the PNG image when the size changed since the header was read.
- Delete the VFS when a pack without an ini has no hash-named textures.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-09-29 10:33:32 -06:00