FlowFieldSceneVerify: the flow as ARPG.unity runs it; parity check on a ramp height and the noise clock
FlowFieldSceneVerify opens ARPG.unity before anything touches the project's types, drives VolumeSampling and Stardust Start/Update with a zone attached, and asserts no exception or logged error, FLOW_GLOBAL and FLOW_SLOPE enabled, _FlowAmplitude non-zero, the stardust buffer changing with the flow and freezing with a paused zone, the noise clock, and CloudShader pass 0 carrying the FLOW_* keywords. FlowFieldParityVerify now binds a ramp height so FLOW_SLOPE and its pragma matter, and checks the noise follows _NebulaNoiseTime. The mutation harness runs both checks, with six new mutants. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
parent
b155003689
commit
819bd79424
5 changed files with 291 additions and 21 deletions
|
|
@ -73,6 +73,7 @@ namespace Aetheria.EditorTools
|
|||
GlobalScrollSpeed = 1f, SlopeAmplitude = 3f, SwirlAmplitude = 5f
|
||||
};
|
||||
CheckParity("wide", settings, work, compute, wide, new[] { 0.0, 1.0, 17.0, 60.0, 100.0 });
|
||||
CheckNoiseClock(compute);
|
||||
|
||||
Debug.Log($"{Tag} max error {_maxError:G4} of amplitude (tolerance {RelativeTolerance:G4})");
|
||||
Debug.Log(_ok ? $"{Tag} PASS" : $"{Tag} FAIL: see errors above");
|
||||
|
|
@ -100,13 +101,13 @@ namespace Aetheria.EditorTools
|
|||
$"{flow.Period}/{flow.SlopeAmplitude}/{flow.SwirlAmplitude}, authored 512/15/0.025/8/0/0");
|
||||
}
|
||||
|
||||
static Zone BuildZone(GameSettings settings, string work, FlowSettings flow)
|
||||
internal static Zone BuildZone(GameSettings settings, string work, FlowSettings flow, double time = 0)
|
||||
{
|
||||
var cache = AetheriaStores.Open(Path.Combine(work, Guid.NewGuid().ToString("N") + ".cc"), catalogWritable: true);
|
||||
cache.Upsert(new VerseGrammar { Revision = 1 });
|
||||
cache.FlushAsync().Wait();
|
||||
var items = new ItemManager(cache, new ProvenanceLedger(), settings.GameplaySettings, _ => { });
|
||||
return new Zone(items, new PlanetSettings { Flow = flow }, new ZonePack(), new GalaxyZone { Name = "Parity", Owner = null }, null);
|
||||
return new Zone(items, new PlanetSettings { Flow = flow }, new ZonePack { Time = time }, new GalaxyZone { Name = "Parity", Owner = null }, null);
|
||||
}
|
||||
|
||||
static void CheckParity(string label, GameSettings settings, string work, ComputeShader compute, FlowSettings flow,
|
||||
|
|
@ -137,8 +138,18 @@ namespace Aetheria.EditorTools
|
|||
var globalBuffer = new ComputeBuffer(count, 12);
|
||||
var slopeBuffer = new ComputeBuffer(count, 12);
|
||||
var gatedBuffer = new ComputeBuffer(count, 12);
|
||||
// flow() reads the nebula's gravity height under FLOW_SLOPE; a flat (all zero) height is the flat zone's.
|
||||
var height = new Texture2D(1, 1, TextureFormat.RFloat, false);
|
||||
ComputeBuffer noiseBuffer = null;
|
||||
// flow() reads the nebula's gravity height under FLOW_SLOPE. A ramp rising along the texture's x gives every
|
||||
// position inside the grid the gradient direction (1, 0), so the slope term is live where a flat height
|
||||
// would zero it (and the FLOW_SLOPE keyword and its pragma would not matter).
|
||||
const int rampSize = 64;
|
||||
var height = new Texture2D(rampSize, rampSize, TextureFormat.RFloat, false)
|
||||
{ filterMode = FilterMode.Point, wrapMode = TextureWrapMode.Clamp };
|
||||
var ramp = new float[rampSize * rampSize];
|
||||
for (var i = 0; i < ramp.Length; i++) ramp[i] = (i % rampSize) / (float) (rampSize - 1);
|
||||
height.SetPixelData(ramp, 0);
|
||||
height.Apply();
|
||||
var rampGradient = new CultMath.float2(1f, 0f);
|
||||
try
|
||||
{
|
||||
positionBuffer.SetData(positions);
|
||||
|
|
@ -149,6 +160,11 @@ namespace Aetheria.EditorTools
|
|||
compute.SetBuffer(kernel, "slopeOut", slopeBuffer);
|
||||
compute.SetBuffer(kernel, "gatedOut", gatedBuffer);
|
||||
compute.SetTexture(kernel, "_NebulaSurfaceHeight", height);
|
||||
compute.SetVector("_NebulaSurfaceHeight_TexelSize", new Vector4(1f / rampSize, 1f / rampSize, rampSize, rampSize));
|
||||
// The grid spans 40000: every sampled position (|xz| to 10000) is inside the ramp's uv range.
|
||||
Shader.SetGlobalVector("_GridTransform", new Vector4(0f, 0f, 40000f, 0f));
|
||||
noiseBuffer = new ComputeBuffer(count, 4);
|
||||
compute.SetBuffer(kernel, "noiseOut", noiseBuffer);
|
||||
compute.SetInt("count", count);
|
||||
|
||||
var elapsed = 0.0;
|
||||
|
|
@ -190,18 +206,19 @@ namespace Aetheria.EditorTools
|
|||
var expected = FlowField.Global(flow, new CultMath.float3(p.x, p.y, p.z), zoneScroll);
|
||||
Compare($"{label}-global", at, i, new Vector3(expected.x, expected.y, expected.z), globalOut[i], flow.GlobalAmplitude);
|
||||
}
|
||||
// flow() is the keyword-gated function the nebula and stardust call; the zone's flat well has a zero
|
||||
// gradient, so Zone.Flow is the global term here. A flow that is live must not read zero.
|
||||
// flow() is the keyword-gated function the nebula and stardust call: the global term plus, under
|
||||
// FLOW_SLOPE, the slope term of the ramp's gradient. A flow that is live must not read zero.
|
||||
var live = 0;
|
||||
var gatedScale = Math.Max(Math.Abs(flow.GlobalAmplitude), Math.Max(Math.Abs(flow.SlopeAmplitude), Math.Abs(flow.SwirlAmplitude)));
|
||||
for (var i = 0; i < positionCount; i++)
|
||||
{
|
||||
var p = positions[i];
|
||||
var expected = zone.Flow(new CultMath.float3(p.x, p.y, p.z));
|
||||
var expected = FlowField.At(flow, new CultMath.float3(p.x, p.y, p.z), zoneScroll, rampGradient);
|
||||
var e = new Vector3(expected.x, expected.y, expected.z);
|
||||
if (e.magnitude > .1f * flow.GlobalAmplitude) live++;
|
||||
Compare($"{label}-flow-keywords", at, i, e, gatedOut[i], flow.GlobalAmplitude);
|
||||
if (e.magnitude > .1f * gatedScale) live++;
|
||||
Compare($"{label}-flow-keywords", at, i, e, gatedOut[i], gatedScale);
|
||||
}
|
||||
if (flow.GlobalAmplitude != 0f && live < positionCount / 4)
|
||||
if (gatedScale != 0f && live < positionCount / 4)
|
||||
Fail($"{label}-flow-keywords", $"{at}: only {live} of {positionCount} zone flows are live; the check is vacuous");
|
||||
var slopeScale = Math.Max(Math.Abs(flow.SlopeAmplitude), Math.Abs(flow.SwirlAmplitude));
|
||||
for (var i = 0; i < gradientCount; i++)
|
||||
|
|
@ -214,7 +231,60 @@ namespace Aetheria.EditorTools
|
|||
}
|
||||
finally
|
||||
{
|
||||
positionBuffer.Release(); gradientBuffer.Release(); globalBuffer.Release(); slopeBuffer.Release(); gatedBuffer.Release(); UnityEngine.Object.DestroyImmediate(height);
|
||||
positionBuffer.Release(); gradientBuffer.Release(); globalBuffer.Release(); slopeBuffer.Release(); gatedBuffer.Release(); noiseBuffer?.Release(); UnityEngine.Object.DestroyImmediate(height);
|
||||
}
|
||||
}
|
||||
|
||||
// The nebula noise the flow advects evolves on the flow clock VolumeSampling lowers as _NebulaNoiseTime, never on
|
||||
// _Time: with the speed non-zero, the noise changes when _NebulaNoiseTime does and not otherwise (a frame clock
|
||||
// would not follow the global set here).
|
||||
static void CheckNoiseClock(ComputeShader compute)
|
||||
{
|
||||
const int count = 64;
|
||||
var kernel = compute.FindKernel("Evaluate");
|
||||
var rng = new System.Random(4217);
|
||||
var positions = new Vector3[count];
|
||||
for (var i = 0; i < count; i++)
|
||||
positions[i] = new Vector3((float) (rng.NextDouble() * 4000 - 2000), (float) (rng.NextDouble() * 100 - 50), (float) (rng.NextDouble() * 4000 - 2000));
|
||||
var buffers = new[] { new ComputeBuffer(count, 12), new ComputeBuffer(count, 8), new ComputeBuffer(count, 12), new ComputeBuffer(count, 12), new ComputeBuffer(count, 12), new ComputeBuffer(count, 4) };
|
||||
var height = new Texture2D(1, 1, TextureFormat.RFloat, false);
|
||||
try
|
||||
{
|
||||
buffers[0].SetData(positions);
|
||||
buffers[1].SetData(new Vector2[count]);
|
||||
compute.SetBuffer(kernel, "positions", buffers[0]);
|
||||
compute.SetBuffer(kernel, "gradients", buffers[1]);
|
||||
compute.SetBuffer(kernel, "globalOut", buffers[2]);
|
||||
compute.SetBuffer(kernel, "slopeOut", buffers[3]);
|
||||
compute.SetBuffer(kernel, "gatedOut", buffers[4]);
|
||||
compute.SetBuffer(kernel, "noiseOut", buffers[5]);
|
||||
compute.SetTexture(kernel, "_NebulaSurfaceHeight", height);
|
||||
compute.SetInt("count", count);
|
||||
Shader.SetGlobalFloat("_NebulaNoiseSpeed", 1f);
|
||||
float[] Noise(float noiseTime)
|
||||
{
|
||||
Shader.SetGlobalFloat("_NebulaNoiseTime", noiseTime);
|
||||
compute.Dispatch(kernel, 1, 1, 1);
|
||||
var result = new float[count];
|
||||
buffers[5].GetData(result);
|
||||
return result;
|
||||
}
|
||||
var a = Noise(0f);
|
||||
var b = Noise(3f);
|
||||
var again = Noise(3f);
|
||||
int moved = 0, repeated = 0;
|
||||
for (var i = 0; i < count; i++)
|
||||
{
|
||||
if (Math.Abs(a[i] - b[i]) > 1e-4f) moved++;
|
||||
if (b[i] == again[i]) repeated++;
|
||||
}
|
||||
if (moved < count / 2) Fail("nebula-noise-clock", $"only {moved} of {count} noise samples follow _NebulaNoiseTime");
|
||||
if (repeated != count) Fail("nebula-noise-clock", $"{count - repeated} of {count} noise samples changed with _NebulaNoiseTime held");
|
||||
}
|
||||
finally
|
||||
{
|
||||
foreach (var buffer in buffers) buffer.Release();
|
||||
UnityEngine.Object.DestroyImmediate(height);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
181
Assets/Scripts/Editor/FlowFieldSceneVerify.cs
Normal file
181
Assets/Scripts/Editor/FlowFieldSceneVerify.cs
Normal file
|
|
@ -0,0 +1,181 @@
|
|||
using System;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Reflection;
|
||||
using UnityEditor;
|
||||
using UnityEditor.SceneManagement;
|
||||
using UnityEngine;
|
||||
using UnityEngine.Rendering;
|
||||
|
||||
namespace Aetheria.EditorTools
|
||||
{
|
||||
/// <summary>
|
||||
/// Batchmode check for the flow as the scene runs it (campaign aetheria-fauna, cut fauna-flow-field; target
|
||||
/// invariants flow-one-owner and flow-shader-parity). FlowFieldParityVerify proves the HLSL against FlowField with
|
||||
/// no scene loaded; this opens ARPG.unity, lets VolumeSampling and Stardust initialise as the game does, drives
|
||||
/// their own Start and Update with a zone attached, and asserts what the presenters did:
|
||||
/// - nothing threw and nothing logged an error (a type that cannot initialise in a scene, as VolumeSampling once
|
||||
/// could when it created its keywords in a static field initializer, throws on its first use);
|
||||
/// - FLOW_GLOBAL and FLOW_SLOPE are enabled, _FlowAmplitude is the zone's, and the nebula noise clock is the zone's;
|
||||
/// - the stardust particle buffer changes with the flow and freezes when the zone pauses;
|
||||
/// - CloudShader pass 0 carries the FLOW_* keywords.
|
||||
/// The scene is opened before any Aetheria type is touched: run this method alone in its own Unity session, never
|
||||
/// after FlowFieldParityVerify (which would initialise VolumeSampling outside a MonoBehaviour constructor).
|
||||
/// Dispatches compute and renders, so it needs a graphics device: run WITHOUT -nographics.
|
||||
/// Unity -batchmode -projectPath <repo> -executeMethod Aetheria.EditorTools.FlowFieldSceneVerify.Run -logFile <path>
|
||||
/// Do not pass -quit: Run exits the Editor itself, 0 on success and 1 on any failure.
|
||||
/// </summary>
|
||||
public static class FlowFieldSceneVerify
|
||||
{
|
||||
const string Tag = "[FlowFieldSceneVerify]";
|
||||
static bool _ok = true;
|
||||
|
||||
static void OnLog(string message, string stack, LogType type)
|
||||
{
|
||||
if ((type == LogType.Error || type == LogType.Exception || type == LogType.Assert) && !message.StartsWith(Tag))
|
||||
Fail("logged-error", message.Split('\n')[0]);
|
||||
}
|
||||
|
||||
static void Fail(string check, string detail)
|
||||
{
|
||||
Debug.LogError($"{Tag} FAIL {check}: {detail}");
|
||||
_ok = false;
|
||||
}
|
||||
|
||||
public static void Run()
|
||||
{
|
||||
if (SystemInfo.graphicsDeviceType == GraphicsDeviceType.Null)
|
||||
{
|
||||
Debug.LogError($"{Tag} FAIL: no graphics device in this session; rerun batchmode without -nographics.");
|
||||
EditorApplication.Exit(1);
|
||||
return;
|
||||
}
|
||||
|
||||
Application.logMessageReceived += OnLog;
|
||||
try
|
||||
{
|
||||
// Before anything else touches the project's types: the scene constructs its MonoBehaviours here.
|
||||
EditorSceneManager.OpenScene("Assets/Scenes/ARPG.unity", OpenSceneMode.Single);
|
||||
Check();
|
||||
Debug.Log(_ok ? $"{Tag} PASS" : $"{Tag} FAIL: see errors above");
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
Debug.LogError($"{Tag} FAIL exception: {e}");
|
||||
_ok = false;
|
||||
}
|
||||
finally
|
||||
{
|
||||
Application.logMessageReceived -= OnLog;
|
||||
}
|
||||
EditorApplication.Exit(_ok ? 0 : 1);
|
||||
}
|
||||
|
||||
static void Check()
|
||||
{
|
||||
var settings = AssetDatabase.LoadAssetAtPath<GameSettings>("Assets/Resources/Settings.asset")
|
||||
?? throw new InvalidOperationException("No Settings.asset.");
|
||||
var agm = UnityEngine.Object.FindObjectsByType<ActionGameManager>(FindObjectsInactive.Include, FindObjectsSortMode.None).FirstOrDefault();
|
||||
var volume = UnityEngine.Object.FindObjectsByType<VolumeSampling>(FindObjectsInactive.Include, FindObjectsSortMode.None).FirstOrDefault();
|
||||
var dust = UnityEngine.Object.FindObjectsByType<Stardust>(FindObjectsInactive.Include, FindObjectsSortMode.None).FirstOrDefault();
|
||||
if (agm == null || volume == null || dust == null)
|
||||
{
|
||||
Fail("scene-objects", $"ARPG.unity has ActionGameManager={agm != null}, VolumeSampling={volume != null}, Stardust={dust != null}");
|
||||
return;
|
||||
}
|
||||
|
||||
// Every flow term live, so FLOW_SLOPE is on; and the same flow with every amplitude zero.
|
||||
var on = new FlowSettings
|
||||
{
|
||||
GlobalScale = 512f, GlobalAmplitude = 15f, GlobalScrollSpeed = .025f, Period = 8f, SlopeAmplitude = 3f, SwirlAmplitude = 5f
|
||||
};
|
||||
var off = new FlowSettings { GlobalScale = on.GlobalScale, GlobalScrollSpeed = on.GlobalScrollSpeed, Period = on.Period };
|
||||
var work = Path.Combine(Path.GetTempPath(), "flow-scene-" + Guid.NewGuid().ToString("N"));
|
||||
Directory.CreateDirectory(work);
|
||||
try
|
||||
{
|
||||
var zone = FlowFieldParityVerify.BuildZone(settings, work, on, 86400.0 * 3 + 12.3);
|
||||
typeof(ActionGameManager).GetProperty("Instance").GetSetMethod(true).Invoke(null, new object[] { agm });
|
||||
typeof(ActionGameManager).GetProperty("Zone").GetSetMethod(true).Invoke(agm, new object[] { zone });
|
||||
if (agm.Settings == null) agm.Settings = settings;
|
||||
|
||||
Call(volume, "Start");
|
||||
Call(volume, "Update");
|
||||
if (!Shader.IsKeywordEnabled("FLOW_GLOBAL")) Fail("scene-keywords", "FLOW_GLOBAL is not enabled after VolumeSampling.Update");
|
||||
if (!Shader.IsKeywordEnabled("FLOW_SLOPE")) Fail("scene-keywords", "FLOW_SLOPE is not enabled after VolumeSampling.Update");
|
||||
if (Shader.GetGlobalFloat("_FlowAmplitude") != on.GlobalAmplitude)
|
||||
Fail("scene-amplitude", $"_FlowAmplitude is {Shader.GetGlobalFloat("_FlowAmplitude")}, the zone's flow has {on.GlobalAmplitude}");
|
||||
var scroll = Shader.GetGlobalVector("_FlowScroll");
|
||||
if (scroll.x != zone.FlowScroll.x || scroll.y != zone.FlowScroll.y)
|
||||
Fail("scene-scroll", $"_FlowScroll ({scroll.x}, {scroll.y}) is not the zone's ({zone.FlowScroll.x}, {zone.FlowScroll.y})");
|
||||
var noiseTime = Shader.GetGlobalFloat("_NebulaNoiseTime");
|
||||
if (noiseTime != (float) zone.ExactTime)
|
||||
Fail("scene-noise-clock", $"_NebulaNoiseTime is {noiseTime}, the zone's time is {(float) zone.ExactTime}");
|
||||
|
||||
CheckCloudKeywords();
|
||||
|
||||
// Stardust follows the flow through its own dispatch: the particle buffer read back each frame.
|
||||
Call(dust, "Start");
|
||||
var bufferField = typeof(Stardust).GetField("_particlesBuffer", BindingFlags.NonPublic | BindingFlags.Instance);
|
||||
float[] Frame()
|
||||
{
|
||||
Call(dust, "Update");
|
||||
var buffer = (ComputeBuffer) bufferField.GetValue(dust);
|
||||
var data = new float[buffer.count * 7];
|
||||
buffer.GetData(data);
|
||||
return data;
|
||||
}
|
||||
var flowing = Frame();
|
||||
VolumeSampling.LowerFlow(off, VolumeSampling.FlowClock);
|
||||
var still = Frame();
|
||||
if (Differing(flowing, still) == 0) Fail("scene-stardust", "the particle buffer is the same with the zone's flow and with no flow");
|
||||
|
||||
// A paused zone freezes the particles and the nebula noise clock; a running one moves both.
|
||||
Call(volume, "Update");
|
||||
zone.Update(0f);
|
||||
Call(volume, "Update");
|
||||
if (Differing(flowing, Frame()) != 0) Fail("scene-stardust-pause", "the particle buffer moved while the zone was paused");
|
||||
if (Shader.GetGlobalFloat("_NebulaNoiseTime") != noiseTime) Fail("scene-noise-clock", "_NebulaNoiseTime moved while the zone was paused");
|
||||
zone.Update(.05f);
|
||||
Call(volume, "Update");
|
||||
if (Differing(flowing, Frame()) == 0) Fail("scene-stardust-run", "the particle buffer did not move while the zone ran");
|
||||
if (Shader.GetGlobalFloat("_NebulaNoiseTime") == noiseTime) Fail("scene-noise-clock", "_NebulaNoiseTime did not move while the zone ran");
|
||||
}
|
||||
finally
|
||||
{
|
||||
try { Directory.Delete(work, true); } catch { }
|
||||
}
|
||||
}
|
||||
|
||||
// CloudShader's pass 0 compiles with the flow keywords (it must #include_with_pragmas the shared include).
|
||||
static void CheckCloudKeywords()
|
||||
{
|
||||
var shader = AssetDatabase.LoadAssetAtPath<Shader>("Assets/Shaders/Raymarching/CloudShader.shader");
|
||||
if (shader == null) { Fail("scene-cloud-keywords", "No CloudShader.shader."); return; }
|
||||
var keywords = ShaderUtil.GetPassKeywords(shader, new PassIdentifier(0, 0), UnityEditor.Rendering.ShaderType.Fragment)
|
||||
.Select(keyword => keyword.name).ToArray();
|
||||
foreach (var name in new[] { "FLOW_GLOBAL", "FLOW_SLOPE" })
|
||||
if (!keywords.Contains(name)) Fail("scene-cloud-keywords", $"CloudShader pass 0 does not carry {name}");
|
||||
}
|
||||
|
||||
static int Differing(float[] a, float[] b)
|
||||
{
|
||||
var count = 0;
|
||||
for (var i = 0; i < a.Length; i++)
|
||||
if (Math.Abs(a[i] - b[i]) > 1e-4f) count++;
|
||||
return count;
|
||||
}
|
||||
|
||||
// The presenters' own Start and Update, as the game calls them; an exception is a failure that names the type.
|
||||
static void Call(object target, string method)
|
||||
{
|
||||
var info = target.GetType().GetMethod(method, BindingFlags.NonPublic | BindingFlags.Public | BindingFlags.Instance);
|
||||
if (info == null) { Fail("scene-presenter-exception", $"{target.GetType().Name}.{method} not found"); return; }
|
||||
try { info.Invoke(target, null); }
|
||||
catch (TargetInvocationException e)
|
||||
{
|
||||
Fail("scene-presenter-exception", $"{target.GetType().Name}.{method} threw {e.InnerException?.GetType().Name}");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
2
Assets/Scripts/Editor/FlowFieldSceneVerify.cs.meta
Normal file
2
Assets/Scripts/Editor/FlowFieldSceneVerify.cs.meta
Normal file
|
|
@ -0,0 +1,2 @@
|
|||
fileFormatVersion: 2
|
||||
guid: 25fbd79fca1de49498c3bebf9f63243b
|
||||
|
|
@ -13,6 +13,7 @@ StructuredBuffer<float2> gradients;
|
|||
RWStructuredBuffer<float3> globalOut;
|
||||
RWStructuredBuffer<float3> slopeOut;
|
||||
RWStructuredBuffer<float3> gatedOut; // flow(): the keyword-gated function the nebula and stardust call
|
||||
RWStructuredBuffer<float> noiseOut; // triNoise3d: the nebula noise the flow advects
|
||||
uint count;
|
||||
|
||||
[numthreads(64, 1, 1)]
|
||||
|
|
@ -22,4 +23,5 @@ void Evaluate(uint3 id : SV_DispatchThreadID)
|
|||
globalOut[id.x] = globalFlow(positions[id.x]);
|
||||
slopeOut[id.x] = slopeFlow(gradients[id.x]);
|
||||
gatedOut[id.x] = flow(positions[id.x]);
|
||||
noiseOut[id.x] = triNoise3d(positions[id.x] / 512);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,8 @@
|
|||
#!/usr/bin/env python3
|
||||
"""Mutation tests for the flow-shader-parity rule (campaign aetheria-fauna, cut fauna-flow-field).
|
||||
|
||||
Assets/Scripts/Editor/FlowFieldSceneVerify.cs opens ARPG.unity and drives VolumeSampling and Stardust as the game
|
||||
does (the keywords, the stardust buffer, CloudShader's keywords); each mutant below names the check that must catch it.
|
||||
Assets/Scripts/Editor/FlowFieldParityVerify.cs evaluates the HLSL globalFlow and slopeFlow
|
||||
(Assets/Shaders/Volumetric.cginc, through FlowParity.compute) and FlowField (ServerShared) for the
|
||||
same positions, scrolls, settings and gradients, after VolumeSampling.LowerFlow has lowered a stepped
|
||||
|
|
@ -25,6 +27,11 @@ from pathlib import Path
|
|||
REPO_ROOT = Path(__file__).resolve().parents[1]
|
||||
UNITY_EXE = r"C:\Program Files\Unity\Hub\Editor\6000.3.24f1\Editor\Unity.exe"
|
||||
|
||||
PARITY = "Aetheria.EditorTools.FlowFieldParityVerify.Run"
|
||||
SCENE = "Aetheria.EditorTools.FlowFieldSceneVerify.Run"
|
||||
STARDUST_COMPUTE = "Assets/Shaders/Compute/Stardust/Stardust.compute"
|
||||
PARITY_COMPUTE = "Assets/Shaders/Compute/FlowParity.compute"
|
||||
CLOUD_SHADER = "Assets/Shaders/Raymarching/CloudShader.shader"
|
||||
CGINC = "Assets/Shaders/Volumetric.cginc"
|
||||
VOLUME_SAMPLING = "Assets/Scripts/Zone Display/VolumeSampling.cs"
|
||||
FLOW_FIELD = "Assets/Scripts/ServerShared/FlowField.cs"
|
||||
|
|
@ -47,14 +54,20 @@ MUTANTS = [
|
|||
("lower-keywords-lookup-only", VOLUME_SAMPLING, 'GlobalKeyword.Create("FLOW_GLOBAL")', 'new GlobalKeyword("FLOW_GLOBAL")', "flow-keywords"),
|
||||
("csharp-scale-doubled", FLOW_FIELD, "var field = position / settings.GlobalScale;", "var field = position / (settings.GlobalScale * 2f);", "global"),
|
||||
("asset-amplitude", SETTINGS_ASSET, " GlobalAmplitude: 15\n", " GlobalAmplitude: 14\n", "authored-flow"),
|
||||
("hlsl-noise-frame-clock", CGINC, "_NebulaNoiseTime * _NebulaNoiseSpeed", "_Time.y * _NebulaNoiseSpeed", "nebula-noise-clock"),
|
||||
("parity-slope-pragma-removed", PARITY_COMPUTE, "#pragma multi_compile __ FLOW_SLOPE", "", "flow-keywords"),
|
||||
("scene-stardust-pragma-removed", STARDUST_COMPUTE, "#pragma multi_compile __ FLOW_GLOBAL", "", "scene-stardust", SCENE),
|
||||
("scene-cloud-include-without-pragmas", CLOUD_SHADER, '#include_with_pragmas "Assets/Shaders/Volumetric.cginc"', '#include "Assets/Shaders/Volumetric.cginc"', "scene-cloud-keywords", SCENE),
|
||||
("scene-keyword-in-field-initializer", VOLUME_SAMPLING, "static GlobalKeyword? _keywordFlowGlobal;", 'static GlobalKeyword? _keywordFlowGlobal = GlobalKeyword.Create("FLOW_GLOBAL");', "scene-presenter-exception", SCENE),
|
||||
("scene-noise-time-frame-clock", VOLUME_SAMPLING, '"_NebulaNoiseTime", (float) clock', '"_NebulaNoiseTime", Time.realtimeSinceStartup', "scene-noise-clock", SCENE),
|
||||
]
|
||||
|
||||
|
||||
def run_probe(log: Path) -> tuple[int, str]:
|
||||
def run_probe(log: Path, entry: str = PARITY) -> tuple[int, str]:
|
||||
log.unlink(missing_ok=True)
|
||||
proc = subprocess.run(
|
||||
[UNITY_EXE, "-batchmode", "-projectPath", str(REPO_ROOT),
|
||||
"-executeMethod", "Aetheria.EditorTools.FlowFieldParityVerify.Run", "-logFile", str(log)],
|
||||
"-executeMethod", entry, "-logFile", str(log)],
|
||||
timeout=1800,
|
||||
)
|
||||
return proc.returncode, log.read_text(encoding="utf-8", errors="replace")
|
||||
|
|
@ -68,14 +81,16 @@ def main() -> int:
|
|||
scratch.mkdir(parents=True, exist_ok=True)
|
||||
failures: list[str] = []
|
||||
|
||||
print("\n=== control: unmutated tree ===")
|
||||
code, text = run_probe(scratch / "control.log")
|
||||
if code != 0 or "[FlowFieldParityVerify] PASS" not in text:
|
||||
print("control FAILED: expected exit 0 and PASS on the unmutated tree")
|
||||
return 1
|
||||
print("control OK: exit 0, PASS")
|
||||
for entry, name in ((PARITY, "FlowFieldParityVerify"), (SCENE, "FlowFieldSceneVerify")):
|
||||
print(f"\n=== control: unmutated tree, {name} ===")
|
||||
code, text = run_probe(scratch / f"control-{name}.log", entry)
|
||||
if code != 0 or f"[{name}] PASS" not in text:
|
||||
print("control FAILED: expected exit 0 and PASS on the unmutated tree")
|
||||
return 1
|
||||
print("control OK: exit 0, PASS")
|
||||
|
||||
for label, rel, before, after, check in MUTANTS:
|
||||
for label, rel, before, after, check, *rest in MUTANTS:
|
||||
entry = rest[0] if rest else PARITY
|
||||
if only and label not in only:
|
||||
continue
|
||||
path = REPO_ROOT / rel
|
||||
|
|
@ -89,7 +104,7 @@ def main() -> int:
|
|||
print(f"\n=== mutant {label} ({rel}) ===")
|
||||
try:
|
||||
path.write_bytes(text_in.replace(b, a).encode("utf-8"))
|
||||
code, text = run_probe(scratch / f"{label}.log")
|
||||
code, text = run_probe(scratch / f"{label}.log", entry)
|
||||
finally:
|
||||
path.write_bytes(original)
|
||||
named = f"FAIL {check}" in text or f"-{check}:" in text
|
||||
|
|
|
|||
Loading…
Reference in a new issue