fix(render): Campaign V slice V7 commit 1 - world anisotropy, and the sky's second clock
Two changes, one measurement. The V6m smoke pair put GL versus Vulkan at Holtburg at 18.52% of the frame differing at tolerance 2 with MSAA off. The same stop on the same instrument now measures 9.05%, and the two populations these address are gone from the difference map rather than merely smaller. 1. THE WORLD ATLASES WERE SAMPLED WITHOUT ANISOTROPY ON VULKAN, AND WITH THE DEVICE MAXIMUM ON GL. RhiWorldTextureArray -- the backend-neutral shared object/material atlas, and the only IWorldTextureArray the Vulkan arm ever constructs -- registered its clamp and repeat slots with GpuSamplerDescription.WorldClamp/WorldRepeat as written, which carry MaxAnisotropy 1. The GL arm asks for the driver's own GL_MAX_TEXTURE_MAX_ANISOTROPY twice over: ManagedGLTextureArray sets GL_TEXTURE_MAX_ANISOTROPY on the image, and the two sampler objects its resident bindless handles are built from (OpenGLGraphicsDevice.WrapSampler/ClampSampler) set it again, which is the one that actually wins. V6i-2 knew it was asking for 1 and said so in a comment -- "the world arm that draws through these arrays is the next slice, and it is the one that can gate a filtering change visually." That slice was V6j, the gate is V7, and this is it. Retail settles the question rather than the GL arm settling it. RenderDeviceD3D::SetDefaultD3DStates (0x005a3800) loops all sixteen sampler stages and issues SetSamplerState(stage, 0xA, this->m_D3DCaps.MaxAnisotropy) at 0x005a4230. 0xA is D3DSAMP_MAXANISOTROPY and the argument is the device's reported cap, not a setting -- so "as much anisotropy as this device has" is retail's own rule, the GL arm is faithful to it, and asking for 1 diverged from retail as well as from the shipping backend. No divergence-register row is owed in either direction: this retires a Vulkan-only gap and lands on retail's value. The fix asks for a ceiling rather than reading a limit back, because the pinned RHI contract (plan section 3.3) carries no anisotropy field and is frozen. It does not need one: VulkanGpuSampler already clamps MaxAnisotropy to VkPhysicalDeviceLimits.maxSamplerAnisotropy, Vulkan guarantees that limit is at least 16 wherever the samplerAnisotropy feature is supported -- which this backend requires -- and 16 is where every desktop driver caps. The request and the GL arm's read therefore land on the same number. What it was worth, from the difference map at the same stop: the roof shingles of both Holtburg cottages, which had been dense hatching across the whole surface, and the stone courses of the near building are now black. Measured as high-frequency energy (mean absolute neighbour difference, GL versus Vulkan) the right-hand roof went from visibly blurred to a ratio of 0.999 and the wall to 1.023; every other textured region in the frame is between 0.99 and 1.02. Grazing-angle surfaces are where anisotropy is the whole difference, which is why a roof was the loudest thing in the frame. 2. THE SKY HAS TWO CLOCKS AND ONLY ONE OF THEM WAS PINNABLE. ACDREAM_DAY_GROUP and the route's AcdreamCycleTimeOfDay presses pin the Dereth date, which chooses the day group, the keyframe and the sun angle. The cloud sheet does not read that clock: SkyRenderer accumulates TexVelocityX/Y against DateTime.UtcNow minus its own construction time, by design, because retail's clouds drift with real time regardless of the date. Two launches minutes apart therefore cannot agree about where the clouds are no matter what the route does, and the V6m smoke measured the cost -- 89% of its 18.52% sat in the top 240 rows. ACDREAM_SKY_PHASE_SECONDS (RuntimeOptions.SkyAnimationPhaseSeconds -> SkyRenderer.AnimationPhaseSecondsOverride) replaces that elapsed-seconds value with a fixed one. Unset -- the default, and every ordinary run -- keeps the wall clock, so nothing a user or the offline gate sees changes. The differential gate forces it on both launches alongside MSAA and the day group; the offline gate keeps its top-280 mask, because a same-commit GL pair still has the sun to disagree about. This is instrument determinism on the same footing as ACDREAM_DAY_GROUP, not a workaround: it is one input to a UV offset, it is off by default, and no shipping path reads it. The alternative on the table was -MaskTopPixels, which would have permanently blinded the campaign's strictest instrument to the entire sky -- one of the five surfaces the offline gate already cannot see. Rows 0-32 of the Holtburg pair went from 23,090 differing pixels to 1,211, and what remains up there is roof and portal rather than cloud. WHAT THE SAME PAIR STILL SHOWS, unattributed and carried to the next commit: the distant treeline, the player and the NPCs, and the animated portal. The portal is phase and expected. The treeline is not filtering -- sharpness now matches within 5% and a shift search finds no sub-pixel offset -- and the two runs entered the world at different last-logout positions (0xC95B0001 versus 0x09040008), so the far-tier streaming history differed. That is the next thing to prove or refute. Gates. Release build green. App tests 4,133 passed / 3 skipped against the 4,132/3 baseline (one new: the sky-phase parse). GL offline pixel gate against the pre-change tree: 2.31e-05, 13 pixels of 563,200, inside the documented 9-31 band -- GL did not move. One offline Vulkan run with VK_LAYER_KHRONOS_validation proven inserted by the loader: zero validation errors, zero warnings. Full three-stop differential recorded at artifacts/v7-diff-c1. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
parent
a7529a975a
commit
ad5f8b68dc
8 changed files with 206 additions and 16 deletions
|
|
@ -493,6 +493,32 @@ AMD's unspecified read was usually returning the resolved image already; and
|
|||
"usually" is exactly the property that makes an unspecified read useless as an
|
||||
instrument.
|
||||
|
||||
**The sky has two clocks, and V7 pinned the second one.**
|
||||
`ACDREAM_SKY_PHASE_SECONDS` (V7, `RuntimeOptions.SkyAnimationPhaseSeconds`,
|
||||
consumed by `SkyRenderer.AnimationPhaseSecondsOverride`) replaces the wall-clock
|
||||
elapsed seconds that `TexVelocityX/Y` accumulate against with a fixed value.
|
||||
**Unset — the default, and every ordinary run — keeps the wall clock**, so
|
||||
nothing the user or the offline gate sees changes unless a gate asks.
|
||||
|
||||
It exists because `ACDREAM_DAY_GROUP` and the `AcdreamCycleTimeOfDay` override
|
||||
pin only the *other* sky clock: the Dereth date, which chooses the day group,
|
||||
the keyframe and the sun angle. The cloud sheet does not read that clock at all
|
||||
and is not supposed to — retail's clouds drift with real time regardless of the
|
||||
date — so a route that pins the world clock still cannot make two launches agree
|
||||
about where the clouds are. That is the whole reason this gate masks its top 280
|
||||
rows, and the V6m smoke pair measured the same population costing **89% of an
|
||||
18.52% whole-frame GL-versus-Vulkan difference**.
|
||||
|
||||
Pinning the phase is instrument determinism rather than a workaround, on the
|
||||
same footing as `ACDREAM_DAY_GROUP`: it is one input to a UV offset, it is off
|
||||
by default, and no shipping code path reads it. The alternative was
|
||||
`-MaskTopPixels`, which would have permanently blinded the campaign's strictest
|
||||
instrument to the whole sky — one of the five surfaces the offline gate already
|
||||
cannot see. **The backend differential gate forces it on both launches**; the
|
||||
offline gate keeps its mask, because a same-commit GL pair has other reasons to
|
||||
disagree up there (the sun moves with the Dereth clock, which that gate does not
|
||||
pin).
|
||||
|
||||
**Coverage.** Terrain and terrain blending, scenery, static world meshes, water,
|
||||
fog, and the entire retained UI (vitals, spell bar, toolbar, chat, radar).
|
||||
|
||||
|
|
|
|||
|
|
@ -1086,6 +1086,11 @@ internal sealed class LivePresentationCompositionPhase
|
|||
// Slice V6k: and V4t's world-handle seam on the device, which
|
||||
// retired the last per-renderer GlBindlessHandleTable.
|
||||
(AcDream.App.Rendering.Gpu.Gl.GlGpuDevice)host.GpuDevice)
|
||||
{
|
||||
// Campaign V slice V7: null unless ACDREAM_SKY_PHASE_SECONDS
|
||||
// is set, which is every run but a differential gate's.
|
||||
AnimationPhaseSecondsOverride = d.Options.SkyAnimationPhaseSeconds,
|
||||
}
|
||||
: new SkyRenderer(
|
||||
host.GpuDevice,
|
||||
host.GpuFrameLifetime,
|
||||
|
|
@ -1093,7 +1098,10 @@ internal sealed class LivePresentationCompositionPhase
|
|||
?? throw new InvalidOperationException(
|
||||
"A backend without a GL context must publish a world pass scope."),
|
||||
content.Dats,
|
||||
foundation.TextureCache),
|
||||
foundation.TextureCache)
|
||||
{
|
||||
AnimationPhaseSecondsOverride = d.Options.SkyAnimationPhaseSeconds,
|
||||
},
|
||||
static value => value.Dispose());
|
||||
// Campaign V slice V6l: particles exist on BOTH arms. The GL arm is
|
||||
// unchanged; the RHI arm compiles the two particle pairs from SPIR-V,
|
||||
|
|
|
|||
|
|
@ -92,6 +92,34 @@ public sealed unsafe partial class SkyRenderer : IDisposable
|
|||
// real time (independent of the day-fraction clock).
|
||||
private readonly DateTime _startedAt = DateTime.UtcNow;
|
||||
|
||||
/// <summary>
|
||||
/// Campaign V slice V7: pins the sky's scroll phase to a fixed number of
|
||||
/// seconds instead of reading the wall clock, so two launches agree.
|
||||
/// <c>null</c> — the default, and what every ordinary run gets — keeps the
|
||||
/// wall clock.
|
||||
///
|
||||
/// <para><b>Why the sky needs its own pin when the world clock is already
|
||||
/// pinnable.</b> Two independent clocks drive this renderer. The Dereth clock
|
||||
/// chooses the day group, the keyframe and the sun angle, and
|
||||
/// <c>AcdreamCycleTimeOfDay</c> freezes it; the differential route presses it
|
||||
/// three times to land on noon. The cloud sheet does not use that clock at
|
||||
/// all — <c>TexVelocityX/Y</c> accumulate against real elapsed time, by
|
||||
/// design, because retail's clouds drift independently of the date. So a
|
||||
/// route that pins the world clock still cannot make two launches agree
|
||||
/// about where the clouds are, and the V6m smoke pair measured what that
|
||||
/// costs: 89% of an 18.52% whole-frame difference lived in the top 240
|
||||
/// rows.</para>
|
||||
///
|
||||
/// <para><b>This is instrument determinism, not a rendering change.</b> It
|
||||
/// alters one input to a UV offset, it is off unless
|
||||
/// <c>ACDREAM_SKY_PHASE_SECONDS</c> is set, and nothing in the shipping
|
||||
/// client reads it. The alternative — masking the sky band in the gate —
|
||||
/// would have permanently blinded the strictest instrument the campaign has
|
||||
/// to the whole sky, which is one of the five surfaces the offline gate
|
||||
/// already cannot see (plan §5.1).</para>
|
||||
/// </summary>
|
||||
internal float? AnimationPhaseSecondsOverride { get; init; }
|
||||
|
||||
// Configurable render distance — retail uses ~1e6; anything larger
|
||||
// than the scene far plane works.
|
||||
public float Near { get; set; } = 0.1f;
|
||||
|
|
@ -288,7 +316,8 @@ public sealed unsafe partial class SkyRenderer : IDisposable
|
|||
// override + transparency fade + luminosity cap.
|
||||
var replaces = PickReplaces(group, dayFraction);
|
||||
|
||||
float secondsSinceStart = (float)(DateTime.UtcNow - _startedAt).TotalSeconds;
|
||||
float secondsSinceStart = AnimationPhaseSecondsOverride
|
||||
?? (float)(DateTime.UtcNow - _startedAt).TotalSeconds;
|
||||
|
||||
for (int i = 0; i < group.SkyObjects.Count; i++)
|
||||
{
|
||||
|
|
|
|||
|
|
@ -197,17 +197,55 @@ internal sealed class RhiWorldTextureArrayFactory(IGpuDevice device) : IWorldTex
|
|||
/// sampler. Both address modes are registered up front because a shared atlas is
|
||||
/// sampled both ways by different batches — the same reason the GL array holds
|
||||
/// two resident bindless handles.</item>
|
||||
/// <item><b>There is no anisotropy knob.</b> The GL array reads
|
||||
/// <c>graphicsDevice.MaxSupportedAnisotropy</c> at construction. The RHI
|
||||
/// sampler takes <see cref="GpuSamplerDescription.MaxAnisotropy"/>, and the
|
||||
/// quality preset does not reach this class yet — the world arm that draws
|
||||
/// through these arrays is the next slice, and it is the one that can gate a
|
||||
/// filtering change visually. Until then this asks for the same trilinear
|
||||
/// filtering with anisotropy 1, and says so rather than guessing.</item>
|
||||
/// <item><b>Anisotropy is asked for as a ceiling rather than read back.</b> See
|
||||
/// <see cref="WorldArrayAnisotropy"/>.</item>
|
||||
/// </list>
|
||||
/// </summary>
|
||||
internal sealed class RhiWorldTextureArray : IWorldTextureArray
|
||||
{
|
||||
/// <summary>
|
||||
/// Campaign V slice V7: the anisotropy the shared world atlases are sampled
|
||||
/// with, and the value that makes this arm's filtering the GL arm's.
|
||||
///
|
||||
/// <para><b>What RETAIL does, which is the same thing.</b>
|
||||
/// <c>RenderDeviceD3D::SetDefaultD3DStates</c> (<c>0x005a3800</c>) loops all
|
||||
/// sixteen sampler stages and, at <c>0x005a4230</c>, issues
|
||||
/// <c>SetSamplerState(stage, 0xA, this->m_D3DCaps.MaxAnisotropy)</c> —
|
||||
/// <c>0xA</c> is <c>D3DSAMP_MAXANISOTROPY</c>, and the value is the device's
|
||||
/// own reported cap rather than a setting. So "as much anisotropy as this
|
||||
/// device has" is retail's rule, not a WorldBuilder habit acdream inherited,
|
||||
/// and asking for 1 here was a divergence from retail as well as from the
|
||||
/// shipping backend.</para>
|
||||
///
|
||||
/// <para><b>What the GL arm does.</b> <c>ManagedGLTextureArray</c> sets
|
||||
/// <c>GL_TEXTURE_MAX_ANISOTROPY</c> to <c>OpenGLGraphicsDevice
|
||||
/// .MaxSupportedAnisotropy</c> — the driver's own
|
||||
/// <c>GL_MAX_TEXTURE_MAX_ANISOTROPY</c>, read once at construction — and its
|
||||
/// two resident bindless handles are built from sampler objects
|
||||
/// (<c>WrapSampler</c>/<c>ClampSampler</c>) that set the same value. So the
|
||||
/// shipping backend asks for "as much anisotropy as this device has,"
|
||||
/// unconditionally, and NOT for the quality preset's level; the preset
|
||||
/// reaches only <c>TerrainAtlas</c>.</para>
|
||||
///
|
||||
/// <para><b>Why a literal rather than a device read.</b> The pinned RHI
|
||||
/// contract (plan §3.3) has no anisotropy limit on
|
||||
/// <see cref="Gpu.GpuCapabilityRecord"/> and is frozen, but it does not need
|
||||
/// one: <c>VulkanGpuSampler</c> already clamps
|
||||
/// <see cref="GpuSamplerDescription.MaxAnisotropy"/> to
|
||||
/// <c>VkPhysicalDeviceLimits.maxSamplerAnisotropy</c>, so requesting a
|
||||
/// ceiling IS requesting the device maximum. Vulkan guarantees that limit is
|
||||
/// at least 16 wherever the <c>samplerAnisotropy</c> feature is supported —
|
||||
/// which this backend requires — and 16 is where every desktop driver caps,
|
||||
/// so the request and the GL arm's read land on the same number.</para>
|
||||
///
|
||||
/// <para><b>Why it is not cosmetic.</b> Measured at V7 on the differential's
|
||||
/// Holtburg stop: with this at 1, Vulkan's roof shingles, distant scenery and
|
||||
/// every grazing-angle surface sample a coarser mip than GL's, which is a
|
||||
/// visible blur and was the largest single population in the first
|
||||
/// GL-versus-Vulkan pair outside the animated sky.</para>
|
||||
/// </summary>
|
||||
private const float WorldArrayAnisotropy = 16f;
|
||||
|
||||
private readonly IGpuDevice _device;
|
||||
private readonly IGpuTexture _texture;
|
||||
private readonly GpuTextureFormat _format;
|
||||
|
|
@ -266,10 +304,16 @@ internal sealed class RhiWorldTextureArray : IWorldTextureArray
|
|||
// ResolveSlot a field read on the hot path.
|
||||
_clampSlot = device.RegisterTexture(
|
||||
texture,
|
||||
device.CreateSampler(GpuSamplerDescription.WorldClamp));
|
||||
device.CreateSampler(GpuSamplerDescription.WorldClamp with
|
||||
{
|
||||
MaxAnisotropy = WorldArrayAnisotropy,
|
||||
}));
|
||||
_wrapSlot = device.RegisterTexture(
|
||||
texture,
|
||||
device.CreateSampler(GpuSamplerDescription.WorldRepeat));
|
||||
device.CreateSampler(GpuSamplerDescription.WorldRepeat with
|
||||
{
|
||||
MaxAnisotropy = WorldArrayAnisotropy,
|
||||
}));
|
||||
}
|
||||
catch
|
||||
{
|
||||
|
|
|
|||
|
|
@ -53,6 +53,7 @@ public sealed record RuntimeOptions(
|
|||
string? UiProbeScript,
|
||||
string? AutomationArtifactDirectory,
|
||||
int? ForcedDayGroupIndex,
|
||||
float? SkyAnimationPhaseSeconds,
|
||||
float FogStartMultiplier,
|
||||
float FogEndMultiplier,
|
||||
ResidencyBudgetOptions ResidencyBudgets,
|
||||
|
|
@ -119,6 +120,18 @@ public sealed record RuntimeOptions(
|
|||
NullIfEmpty(env("ACDREAM_AUTOMATION_ARTIFACT_DIR")),
|
||||
ForcedDayGroupIndex:
|
||||
TryParseNonNegativeInt(env("ACDREAM_DAY_GROUP")),
|
||||
// Campaign V slice V7 instrument determinism: pins the sky's UV
|
||||
// scroll phase — the cloud sheet — to a fixed elapsed-seconds value
|
||||
// instead of the wall clock, so two launches of the differential
|
||||
// gate agree about where the clouds are. ACDREAM_DAY_GROUP and the
|
||||
// AcdreamCycleTimeOfDay override pin the OTHER sky clock (day group,
|
||||
// keyframe, sun angle); this one is independent of both by design,
|
||||
// because retail's clouds drift with real time rather than with the
|
||||
// date. Unset — the default and every ordinary run — keeps the wall
|
||||
// clock. Negative values are accepted: the offset is taken modulo 1
|
||||
// per axis, so any finite number is a valid phase.
|
||||
SkyAnimationPhaseSeconds:
|
||||
TryParseFloat(env("ACDREAM_SKY_PHASE_SECONDS")),
|
||||
FogStartMultiplier: TryParseFloat(env("ACDREAM_FOG_START_MULT")) ?? 0.7f,
|
||||
FogEndMultiplier: TryParseFloat(env("ACDREAM_FOG_END_MULT")) ?? 0.95f,
|
||||
ResidencyBudgets: ResidencyBudgetOptions.Parse(env),
|
||||
|
|
|
|||
|
|
@ -95,8 +95,22 @@ public sealed class RhiWorldTextureArrayTests
|
|||
.Where(registration => registration.TextureName.StartsWith("world-atlas", StringComparison.Ordinal))
|
||||
.Select(registration => registration.Sampler),
|
||||
];
|
||||
Assert.Contains(GpuSamplerDescription.WorldClamp, samplers);
|
||||
Assert.Contains(GpuSamplerDescription.WorldRepeat, samplers);
|
||||
// Campaign V slice V7: both address modes, and BOTH anisotropic. The GL
|
||||
// array asks for GL_TEXTURE_MAX_ANISOTROPY = the driver's maximum on
|
||||
// every world atlas, unconditionally; asking for 1 here made Vulkan
|
||||
// sample a coarser mip than GL on every grazing-angle surface, which the
|
||||
// first GL-versus-Vulkan pair saw as blurred roof shingles and distant
|
||||
// scenery. `with { MaxAnisotropy = 1f }` recovers the campaign's shared
|
||||
// description so this stays an assertion about ONE dimension.
|
||||
Assert.Contains(GpuSamplerDescription.WorldClamp, samplers.Select(Isotropic));
|
||||
Assert.Contains(GpuSamplerDescription.WorldRepeat, samplers.Select(Isotropic));
|
||||
Assert.All(samplers, sampler => Assert.True(
|
||||
sampler.MaxAnisotropy > 1f,
|
||||
$"world atlas sampler {sampler.AddressU} asked for anisotropy "
|
||||
+ $"{sampler.MaxAnisotropy}; the GL arm always asks for the device maximum."));
|
||||
|
||||
static GpuSamplerDescription Isotropic(GpuSamplerDescription sampler) =>
|
||||
sampler with { MaxAnisotropy = 1f };
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
|
|
|||
|
|
@ -358,6 +358,39 @@ public sealed class RuntimeOptionsTests
|
|||
.ForcedDayGroupIndex);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Campaign V slice V7. The sky has two clocks and this pins the one
|
||||
/// ACDREAM_DAY_GROUP cannot reach — the cloud sheet's UV scroll, which
|
||||
/// accumulates against real elapsed time. Unset must stay unset: every
|
||||
/// ordinary run keeps the wall clock, and only the backend differential gate
|
||||
/// asks for a pin.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void SkyPhaseOverride_IsReadOnceIntoTypedOptions()
|
||||
{
|
||||
Assert.Null(
|
||||
RuntimeOptions.Parse(AnyDatDir, EmptyEnv()).SkyAnimationPhaseSeconds);
|
||||
Assert.Null(
|
||||
RuntimeOptions.Parse(
|
||||
AnyDatDir,
|
||||
Env(new() { ["ACDREAM_SKY_PHASE_SECONDS"] = "not-a-number" }))
|
||||
.SkyAnimationPhaseSeconds);
|
||||
// Zero is a real request, not "unset": it is the cloud sheet's authored
|
||||
// origin and the value the gate script pins by default.
|
||||
Assert.Equal(
|
||||
0f,
|
||||
RuntimeOptions.Parse(
|
||||
AnyDatDir,
|
||||
Env(new() { ["ACDREAM_SKY_PHASE_SECONDS"] = "0" }))
|
||||
.SkyAnimationPhaseSeconds);
|
||||
Assert.Equal(
|
||||
12.5f,
|
||||
RuntimeOptions.Parse(
|
||||
AnyDatDir,
|
||||
Env(new() { ["ACDREAM_SKY_PHASE_SECONDS"] = "12.5" }))
|
||||
.SkyAnimationPhaseSeconds);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void DiagnosticFlags_RespectExactValueOne()
|
||||
{
|
||||
|
|
|
|||
|
|
@ -15,7 +15,7 @@
|
|||
MSAA off, ACDREAM_DAY_GROUP pinned,
|
||||
at every deterministic checkpoint of the connected route.
|
||||
|
||||
THREE THINGS ARE FORCED, and each is load-bearing.
|
||||
FOUR THINGS ARE FORCED, and each is load-bearing.
|
||||
|
||||
1. ACDREAM_MSAA_SAMPLES=0 ON BOTH LAUNCHES. Multisample resolve positions
|
||||
are explicitly unspecified across implementations. Plan section 5.5.16
|
||||
|
|
@ -31,7 +31,20 @@
|
|||
with wall time; two launches minutes apart cannot agree about either.
|
||||
The route additionally pins the client-only time-of-day override.
|
||||
|
||||
3. A DESKTOP WITNESS per run, with the guards the repeat-run gate
|
||||
3. ACDREAM_SKY_PHASE_SECONDS pinned ON BOTH LAUNCHES. The day group and the
|
||||
time-of-day override pin ONE of the sky's two clocks -- the one that
|
||||
chooses the keyframe and the sun angle. The cloud sheet runs on the
|
||||
other: TexVelocityX/Y accumulate against real elapsed time by design,
|
||||
because retail's clouds drift independently of the date, so no amount of
|
||||
world-clock pinning makes two launches agree about where they are. The
|
||||
V6m smoke pair measured the cost of leaving it: 89% of an 18.52%
|
||||
whole-frame difference lived in the top 240 rows. Pinning the phase is
|
||||
instrument determinism -- it is off in every ordinary run, it changes one
|
||||
input to a UV offset, and it is what lets this gate keep the SKY under
|
||||
strict comparison instead of masking it, which is what -MaskTopPixels
|
||||
would have cost.
|
||||
|
||||
4. A DESKTOP WITNESS per run, with the guards the repeat-run gate
|
||||
(run-repeat-connected-gate.ps1) learned the hard way. The client's own
|
||||
capture is the differential's subject, but a blank frame reads back as a
|
||||
valid PNG (plan section 5.5.2), so a second instrument that shares
|
||||
|
|
@ -71,6 +84,11 @@
|
|||
.PARAMETER DayGroup
|
||||
Sky day-group index pinned on both launches. Default 0.
|
||||
|
||||
.PARAMETER SkyPhaseSeconds
|
||||
Elapsed-seconds value the sky's UV scroll is pinned to on both launches.
|
||||
Default 0 -- the cloud sheet's authored origin. Any finite value works: the
|
||||
offset is taken modulo 1 per axis. See forced item 3 above.
|
||||
|
||||
.PARAMETER MaskTopPixels
|
||||
Height in pixels of a top band excluded from every comparison. DEFAULT 0 --
|
||||
nothing is masked, and the gate is a strict identity check on the whole
|
||||
|
|
@ -112,6 +130,7 @@ param(
|
|||
[int]$Tolerance = 2,
|
||||
[double]$MaxDifferentFraction = 0.001,
|
||||
[int]$DayGroup = 0,
|
||||
[double]$SkyPhaseSeconds = 0,
|
||||
[int]$MaskTopPixels = 0,
|
||||
[int]$MinRenderedBytes = 500000,
|
||||
[int]$CooldownSeconds = 15,
|
||||
|
|
@ -191,8 +210,10 @@ function Invoke-BackendRun([string]$Backend) {
|
|||
$env:ACDREAM_RETAIL_UI = '1'
|
||||
$env:ACDREAM_DEVTOOLS = '0'
|
||||
$env:ACDREAM_DAY_GROUP = "$DayGroup"
|
||||
# Both launches, always. See the .DESCRIPTION note 1.
|
||||
# Both launches, always. See the .DESCRIPTION notes 1 and 3.
|
||||
$env:ACDREAM_MSAA_SAMPLES = '0'
|
||||
$env:ACDREAM_SKY_PHASE_SECONDS =
|
||||
$SkyPhaseSeconds.ToString([System.Globalization.CultureInfo]::InvariantCulture)
|
||||
$env:ACDREAM_RENDER_BACKEND = $Backend
|
||||
$env:ACDREAM_UI_PROBE_SCRIPT = $effectiveRoute
|
||||
$env:ACDREAM_AUTOMATION_ARTIFACT_DIR = $dir
|
||||
|
|
@ -253,6 +274,7 @@ function Invoke-BackendRun([string]$Backend) {
|
|||
|
||||
Remove-Item Env:\ACDREAM_RENDER_BACKEND -ErrorAction SilentlyContinue
|
||||
Remove-Item Env:\ACDREAM_MSAA_SAMPLES -ErrorAction SilentlyContinue
|
||||
Remove-Item Env:\ACDREAM_SKY_PHASE_SECONDS -ErrorAction SilentlyContinue
|
||||
|
||||
$strayInput = @(Select-String -Path $log -Pattern 'ScrollUp|ScrollDown|ZoomIn|ZoomOut|CameraZoom' `
|
||||
-CaseSensitive -ErrorAction SilentlyContinue)
|
||||
|
|
@ -362,6 +384,7 @@ $report = [pscustomobject][ordered]@{
|
|||
MaxDifferentPixelFraction = $MaxDifferentFraction
|
||||
MsaaSamples = 0
|
||||
DayGroup = $DayGroup
|
||||
SkyPhaseSeconds = $SkyPhaseSeconds
|
||||
MaskTopPixels = $MaskTopPixels
|
||||
Runs = @($gl, $vk)
|
||||
Pairs = @($pairs)
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue