…rrays (port Eglantine-Community#354, fixes the dark water grid) (#1485)
* shader: don't track bindless handles through texture results or float math
The guest compiler reuses registers: in Ocarina of Time's clustered
lighting shaders the register holding a light's shadow map handle holds
a shadow sample's result on another path, so the handle reaching a later
sample is a Phi of the handle and that result. Track then searched
through the sample's coordinates and returned the first constant buffer
word behind them: a float (0.001 in cbuf6+0x430), bound as a texture
handle (a TIC index far past the pool, a null view).
A handle is never made by a texture instruction or by floating-point
math, so the search no longer looks into either, and leaves such a
handle to the handle program builder.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* shader, video_core: index a descriptor array with a handle table's per-invocation index
Ocarina of Time's clustered lighting (330 world fragment shaders) loops
over the lights of each pixel's cluster: it reads a light index from the
pixel's own list, then that light's shadow map handle from a table of
96-byte light entries (the first word of a 16-byte load), and samples
the shadow map with depth comparison. No single handle serves a draw,
so the handle program builder gave up (on CompositeExtractU32x4, and on
the per-pixel list address behind it) and the earlier search through
loads bound the constant buffer word holding the table's address
(cbuf0+0x520) as the handle: an ASTC color texture, an R32F or
A2B10G10R10_UINT render target, or null, depending on the address. A
depth-compare sample of a color or integer image is undefined. On AMD it
is the likely source of the neon green that the probe captures picked up
and the GI fed back into the scene, the tint growing over time.
Such a handle now gets a descriptor array, as a handle read from a
constant buffer at a dynamic offset does:
- The builder handles CompositeExtractU32x3/x4 like the x2 case.
- It may take one word as the array index: a word read at an address it
can't compute (each invocation's own), scaled to a byte offset, that
gives the address of the read holding the handle.
- The program computes the handle of each element through
HandleOp::Index, so the draw binds the first 64 table entries
(LICHII_BINDLESS_TABLE, 1 to 1024; 0 turns these arrays off).
- The shader indexes the array with the index word it read (NonUniform
where supported), clamped to the array, provided the word is defined
wherever the texture instruction runs. A dominator tree of the program
decides that; otherwise every invocation takes the first element.
- The texture cap divides the descriptor budget by arrays rather than by
the instructions sharing them.
- Programs drop trailing Tees, so instructions reading the same handle
share a descriptor. Every light's samples in a shader now share one
64-element array.
Per draw, the table is read in one go: each constant buffer word and
storage buffer address once, with one sampler lookup per run of equal
samplers. FillImageViews also skips an element repeating the view before
it. The first 64 changes in what a table holds are logged ("Handle
table: ... distinct: ticN@element"), as are any handles still left to
the earlier search, and the startup log reports LICHII_BINDLESS_TABLE.
An offline sweep of the game's 1768 fragment and 493 compute shaders
finds no handle left to the earlier search (4510 before). The 5670 shadow
samples now read the table, the other shaders' descriptors are
unchanged, and all the SPIR-V validates.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* vulkan: bind the null image for depth-compare samples of uncomparable images
Sampling with depth comparison requires an image view format with
VK_FORMAT_FEATURE_2_SAMPLED_IMAGE_DEPTH_COMPARISON_BIT. Without it the
result is undefined and differs between vendors, and a garbage value
(even a NaN) can survive a shader multiplying the shadow factor by zero.
A depth-compare descriptor now gets the null image instead in two cases:
- The handle was read from memory (a handle program, such as an unused
entry of a light table).
- The image is an integer or block-compressed color texture, which no
game compares on purpose.
A single-channel color texture behind a constant buffer handle keeps its
view, since a game may compare it deliberately.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* texture_cache: visit a repeated view again once the pass has deleted views
FillImageViews gives an element that repeats the one before it that
element's view instead of visiting it again. A deletion earlier in the
same pass can take that view with it: scaling a blacklisted image down
erases the image's views (InvalidateScale), and a following element with
the same index, blacklisted too, then read the erased slot. Once the
pass has deleted views, such an element is visited again, as every
element was before. The pass repeats after a deletion anyway, and the
next one takes the shortcut again.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* shader: make handle table arrays a host capability, which OpenGL lacks
OpenGL pipelines bind at most 64 textures across their stages
(MAX_TEXTURES), in fixed-size arrays, while the OpenGL host's descriptor
limits are the 1024 defaults, so the texture pass doesn't hold its arrays
to that. A 64-element handle table array and any other texture would fail
the pipeline's assert and overrun those arrays. Handle table arrays are
now made only for a host with HostTranslateInfo::support_handle_table_arrays:
Vulkan and the offline shader sweep. On OpenGL such a handle keeps the
constant buffer word the earlier search finds, as before them. A test
covers both.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* shader: fit texture arrays to the sampler limits of the stage and pipeline
A texture is a combined image sampler in Vulkan, so it counts against
maxPerStageDescriptorSamplers and maxDescriptorSetSamplers as well as
the sampled image limits. LimitSampledTextureArrays checked only the
sampled image and resource limits, so a 64-element handle table array
could exceed a device's sampler limits and make an invalid pipeline
layout. The descriptor set limits also count the descriptors of every
stage, while each stage sized its arrays as if it had the set to itself.
Texture arrays are now capped by the sampler limits too (texel buffers
still count only as sampled images), and before its stages are emitted
a graphics pipeline shrinks the arrays of all its stages together until
their textures fit the set limits. The Vulkan host passes
maxPerStageDescriptorSamplers in HostTranslateInfo. A test covers the
per-stage and pipeline limits.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* shader-sweep: index sampled image arrays nonuniformly, as Vulkan does
The offline profile enables handle table arrays like the Vulkan host,
but not sampled image array nonuniform indexing, so it emitted their
per-invocation accesses without the NonUniform decoration. It now enables
it, matching the SPIR-V the Vulkan backend emits for these arrays.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* vulkan: make handle table arrays only with nonuniform sampled image indexing
A handle table array is indexed with a word each invocation reads for
itself. Without shaderSampledImageArrayNonUniformIndexing the index must
be dynamically uniform, and the emitter can't decorate the access
NonUniform, so on such a device the array would be read with undefined
results. The Vulkan host now enables handle table arrays only when the
device supports that feature; otherwise such a handle keeps the constant
buffer word the earlier search finds, as on OpenGL.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* vulkan: fit a dual vertex program's texture arrays to the stage limits
Vertex A and Vertex B each fit their texture arrays to the per-stage
descriptor limits when they are translated, but merged they are one
stage, and nothing checked that stage: the pipeline pass only checks
the descriptor set limits. The merged program now goes through
LimitSampledTextureArrays as well.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* shader: join the handle programs of a dual vertex program's halves
MergeDualVertexPrograms starts from Vertex A's Info and adds Vertex B's
descriptors, which kept the indices of Vertex B's handle programs but
not the programs. Such a descriptor indexed Vertex A's list, past its
end when Vertex A has none, and Vertex B's programs read storage buffers
at Vertex B's bindings, which JoinStorageInfo renumbers.
JoinStorageInfo now returns the binding each buffer gets in the merged
program, and JoinTextureInfo appends Vertex B's handle programs with
those bindings, once each as the texture pass keeps them, and remaps
the program index of Vertex B's descriptors. A test merges two programs
whose storage buffers swap bindings.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* shader: point Vertex B's instructions at its joined descriptors and buffers
Joining Vertex B's descriptors and storage buffers onto Vertex A's can
give them new indices, but Vertex B's instructions kept the ones from
its own lists. Whenever Vertex A had descriptors or buffers of the same
kind, Vertex B's samples, image accesses, rescaling tests and storage
loads, stores and atomics read Vertex A's.
JoinTextureInfo now returns the index each of Vertex B's descriptors
gets in the merged lists, and MergeDualVertexPrograms renumbers Vertex
B's blocks with those and the bindings JoinStorageInfo returns. Vertex
A's instructions keep their indices. Tests merge programs whose
descriptors and buffers move, and remap each kind directly.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* shader: count texture arrays as the descriptors they become
The texture pass's first cap counted arrays by their constant buffer
word, handle program, type and depth, so arrays that Descriptors::Add
keeps apart, such as one table sampled with two samplers, counted once
and each got the whole budget. They are now counted by the fields
Descriptors::Add compares, all but the count, which the cap sets.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* shader: count texture arrays by the type the texture pass gives them
The first cap counted texture arrays by the type each instruction had
before the replace loop, which sets some types only later: a size query
carries none until it takes the type of another instruction with the
same handle program or the one its handle reads as, and a 1D fetch of a
texel buffer becomes a buffer. A size query and a 2D sample of one table
counted as two arrays and halved the cap. The count now uses the type
the replace loop gives, from one helper both share.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* vulkan: log handle tables only when EDEN_NX2_HANDLE_TABLE_NOTES is set
HandleTableNotes logged until 64 lines were written, but a table that
doesn't change writes no line, so in steady play every draw and dispatch
with a handle table kept copying its texture indices and taking the notes'
lock for a log that never came. The notes are a diagnostic: they are now
off unless the switch is set, and the 64-line limit stays.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* shader: count texture arrays cut to one element, merge descriptors a cut makes equal
A texture array that the limits cut to a single element looked like a single texture. The
stage check (LimitSampledTextureArrays) skipped a program whose arrays the pass had already
cut to one element, and the pipeline check did the same for arrays a stage had cut, so a
stage (compute included) or a pipeline over the limits reached layout creation unchecked.
TextureDescriptor::is_descriptor_array now records that the shader indexes the descriptor,
and both checks count such arrays whatever their count. A program that reads its textures
only on their own is left as before.
A cut also left equal descriptors that the pass would have merged: an array cut to the
elements of another descriptor of its texture, or to the single texture a descriptor of its
own reads. The limits counted both and could fail a stage that fits. One element has no
stride, so an array cut to one takes a single texture's, and CapTextureArrays merges the
descriptors that are then equal, pointing the instructions and the rescaling tests at the
one kept.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* shader, vulkan: fit handle table arrays to nx2's mesh fallback and cache records
Merging nx2 brought code that the handle table arrays meet for the first
time:
- Mesh and task emulation through compute (Eglantine-Community#569, Eglantine-Community#841). Task and mesh
programs run in one host compute stage together with the fallback's
storage buffers. Their arrays now fit that stage's sampler, sampled
image and resource limits (LimitSharedStageSampledTextureArrays), and
the fallback also checks the stage's sampler limit. The pipeline-wide
limit runs before the fallback, because cutting arrays can merge
descriptors.
- The fallback numbered its output buffer's binding by descriptor
elements. An array takes one binding (DefineTextures and the
descriptor layout builder), so it now counts descriptors. With arrays
of one element, as before this branch, both counts agree.
- The environment read properties of cache records (Eglantine-Community#815) now include
support_handle_table_arrays. A record made with handle table arrays
reads other environment words than one made without them.
- The shader sweep fits the arrays of a pipeline's emitted stages to one
descriptor set, as the runtime does.
Tests: handle table support changes the environment read properties;
task and mesh arrays share the compute stage budget (limited by
resources, by samplers, and too small even after cutting); joined
storage descriptors keep Eglantine-Community#816's write bounds of both halves. The handle
unit tests pass with a small Catch2 runner on Linux: 23 cases, 386
assertions.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* video_core: track all 64 storage buffer bases when reading a handle table
EvaluateTextureHandles reads each storage buffer's base once for all the
elements of a table, and kept which bases it had read in a 32-bit mask.
Native programs bind up to Info::MAX_SSBOS (64) storage buffers, and
Ocarina of Time binds 37. 1U << binding is undefined from binding 32 on;
on x86 it wraps, so binding 33 took binding 1's bit, counted its base as
read, and read the handle from an uninitialized base. EvaluateTextureHandle,
for one element, has no mask and was right.
The mask is 64 bits now. A test reads binding 1, then binding 33, for two
elements: it got a wrong handle before and passes now.
Found by CodeRabbit.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* texture_pass: fix the handle table size at 64 entries, drop LICHII_BINDLESS_TABLE
Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>
* vulkan: drop branch hints from the handle table notes checks
Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>
---------
Co-authored-by: jpkto <255363096+jpkto@users.noreply.github.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>
Summary
The update checker inherited from Eden was switched off in CI and still pointed at Eden's servers. This makes it work for Eglantine's tagged releases.
vX.Y.ZandvX.Y.Z-rcNtags count; daily and branch builds are ignored and do not check for updates themselves. (Branch builds will be addressed in Tester builds: follow a branch or PR and build it on the user's machine #842)userfolder is never touched. Other packages (tar.gz, macOS) save the download to the Downloads folder.eglantine-updater.exein the Windows zips.The installer and the Windows helper are ported from Lichii (GPL-3.0-or-later), and the channel model follows Orchid's. Three fixes over Lichii's code: the helper now really links the C runtime statically, manifest lines like
./user/xcan no longer get past theuserfolder protection, and an empty staging folder is refused instead of wiping the install.Notes for reviewers:
builtin_news.cpp) used the same Eden release endpoints, so it now reads this repo's releases too.Type of change
Testing
Windows, MSVC build faked as release
v0.1.0-rc2in a portable folder:userfolder unchanged all workedteststarget does not currently build with MSVC.ENABLE_UPDATE_CHECKERboth on and off.Games tested:
Not applicable, no emulation changes.
Checklist
Screenshots / logs
AI Disclosure: Orchestrated by Fable 5.2 driving other Claude models
Summary by CodeRabbit