Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
57 changes: 57 additions & 0 deletions docs/roadmaps/game-area-portals.md
Original file line number Diff line number Diff line change
Expand Up @@ -258,6 +258,63 @@ product installation or retained adoption. P3 remains open for performance and
fresh/retained product adoption. Original resume04/resume05 effects, caches and custody
are unchanged and were not reused.

### P3 current presentation performance smoke source — 2026-10-10

An explicit `--presentation-performance` mode requires a declared exact source revision
and emits raw stock counters plus its actual assembly and workload digests. It creates
and validates the existing five-tick scene once, disposes its worlds, warms up five
cycles, and then measures twenty samples of one hundred twelve-view cycles: 24,000
actual interpolation and scene-projection views over immutable copied snapshots.
Each returned frame is retained and checked outside timing against qualified bindings,
identity/point coverage and structural bounds. Default and presentation modes remain.

The mode reports elapsed timestamps, process CPU ticks and GC collection deltas,
and allocations on the synchronous measurement thread, with per-view normalization
and sample ranges/medians. Loop/storage/counter overhead is included; setup, validation
and JSON serialization are excluded. Process CPU includes runtime threads and has
coarse resolution. No subtraction, timing threshold or speedup verdict is invented.

Direct optimized F# compilation against the already qualified public assemblies passed
with warnings as errors. Six compiled refusal controls passed before world setup:
missing, short, uppercase and nonhex declared source revisions, an invalid argument,
and an extra performance argument all exited 2; the existing usage substring remains.
One separately admitted fresh offline receiver then passed at source
`13a8cdeb821e43bcd94701902d5b6b2a3e6f42d4`. It used only the seven newly owned,
previously public-qualified package payloads in an isolated local feed and empty new
caches. Locked restore preserved the genuine lock, the actual package boundary passed,
and Release compilation reported no warnings or errors. Default/presentation/invalid
entrypoints exited 0/0/2; the explicit performance entrypoint exited 0.

All twenty raw samples and 24,000 checked views were retained under .NET `10.0.12`,
SDK `10.0.401`, CPU 1 and a 1.5 GiB managed heap cap. Elapsed time per view had a
median of 886.82 ns and a range of 777.68–9,159.78 ns. Process CPU per view had a
median of 885 ns and a range of 776.67–9,117.5 ns. Measurement-thread allocations
per view had a median of 1,970.07 bytes and a range of 1,970.07–1,970.17 bytes.
Process collection deltas summed to 2/1/0 for generations 0/1/2. Outliers were retained;
there was no rerun. These are scoped current smoke observations, not a budget verdict,
speedup claim or proof of steady-state behavior. Workload SHA256 is
`66113918829ada39ae1a4fdfd32255e300e3986db4bbabc0b6e69807f34d4fd4`;
the measured assembly SHA256 is
`cd1fcc61c20cce56e37cbfcf0a382d27eb74642f39d8af79c61f69b76b2530d1`.

The original pre-edit performance baseline remains unknown. The receiver completed
in 3.32 seconds; that whole-operation duration is separate from the view samples.
A separate post-terminal census found no owned process-group/session member;
total RSS and detached descendants were not independently measured. Physics stepping,
raster/GPU timing and installed-product/adoption are outside the measured scope.
Performance release requirements and fresh/retained product adoption remain open;
P3 is not done.
Original resume04/resume05 effects and custody remain unchanged.

Native PR709 checks on Ubuntu and Windows subsequently failed compilation at the
nullable source-revision guard; the surface-baseline job stopped at the same build,
before baseline refresh. The guard now uses `Option.ofObj` to refine the environment
value before string operations. Direct compilation with null checking, latest language
version and warnings as errors passes, as do the six compiled refusal controls.
The measured computational body is byte-identical to source `13a8cdeb`; no measurement
was repeated. The raw observations above remain bound to that original source and
assembly, rather than certifying the successor assembly's exact timing.

A crate straddling an opening and colliding on both sides, or a rope/joint spanning
areas, requires a separately designed and measured solver/constraint strategy. Body
recreation does not preserve hidden contacts, warm-start state or exact solver rollback.
Expand Down
96 changes: 96 additions & 0 deletions examples/Box2D.Portals/Program.fs
Original file line number Diff line number Diff line change
@@ -1,5 +1,12 @@
module Box2D.Portals.Program

open System
open System.Diagnostics
open System.IO
open System.Security.Cryptography
open System.Text
open System.Text.Json

let private qualifyDefault () =
let observed = Scene.qualify ()
let event = observed.Head.Traversals.Head
Expand All @@ -10,6 +17,87 @@ let private qualifyDefault () =
event.DestinationPose.LinearVelocity.X event.DestinationPose.LinearVelocity.Y event.DestinationPose.AngularVelocity
0

let private sha256 (bytes: byte array) = SHA256.HashData bytes |> Convert.ToHexStringLower

let private qualifyPerformance sourceRevision =
// Worlds are disposed inside qualify, before any warmup or timed view invocation.
let snapshots, referenceFrames = Presentation.qualify ()
if snapshots.Length <> 5 || referenceFrames.Length <> 12 then
failwith "Performance workload must have five snapshots and twelve reference frames."
let inputs =
snapshots |> List.pairwise |> List.collect (fun (previous, current) ->
[ for alpha in [ 0.; 0.5; 1. ] do yield previous, current, alpha ]) |> List.toArray
let expected = List.toArray referenceFrames
let check index (frame: Presentation.Frame) =
let _, current, _ = inputs[index % inputs.Length]
let nodes, points = Presentation.inspect current frame
let reference = expected[index % expected.Length]
let bindings (value: Presentation.Frame) = value.Areas |> List.map (fun area -> area.Area, area.Bindings)
if frame.Tick <> reference.Tick || frame.Alpha <> reference.Alpha || bindings frame <> bindings reference then
failwith "Measured view differs from the qualified reference."
if nodes < 1 || nodes > 2 || points <> 2 then failwith "Performance structural bounds differ."
nodes, points
for index in 0 .. expected.Length - 1 do check index expected[index] |> ignore
for _ in 1 .. 5 do
for index in 0 .. inputs.Length - 1 do
let previous, current, alpha = inputs[index]
Presentation.view alpha previous current |> check index |> ignore

// Storage is allocated once outside all timed intervals. Every returned frame is
// retained and inspected afterwards; inspection/serialization are not timed.
let frames = Array.zeroCreate<Presentation.Frame> (100 * inputs.Length)
use measuredProcess = Process.GetCurrentProcess ()
let samples =
[| for sample in 1 .. 20 do
let collectionsBefore = [| for generation in 0 .. 2 -> GC.CollectionCount generation |]
let cpuBefore = measuredProcess.TotalProcessorTime.Ticks
let allocationBefore = GC.GetAllocatedBytesForCurrentThread ()
let started = Stopwatch.GetTimestamp ()
for index in 0 .. frames.Length - 1 do
let previous, current, alpha = inputs[index % inputs.Length]
frames[index] <- Presentation.view alpha previous current
let elapsedTicks = Stopwatch.GetTimestamp () - started
let allocatedBytes = GC.GetAllocatedBytesForCurrentThread () - allocationBefore
let processCpuTicks = measuredProcess.TotalProcessorTime.Ticks - cpuBefore
let collections = [| for generation in 0 .. 2 -> GC.CollectionCount generation - collectionsBefore[generation] |]
let mutable areaNodes = 0
let mutable points = 0
for index in 0 .. frames.Length - 1 do
let nodes, count = check index frames[index]
areaNodes <- areaNodes + nodes
points <- points + count
if areaNodes <> 2400 || points <> 2400 then failwith "Measured workload totals differ."
yield
{| sample = sample; views = frames.Length; elapsedTicks = elapsedTicks
elapsedNanosecondsPerView = float elapsedTicks * 1e9 / float Stopwatch.Frequency / float frames.Length
processCpuTicks = processCpuTicks
processCpuNanosecondsPerView = float processCpuTicks * 100. / float frames.Length
currentThreadAllocatedBytes = allocatedBytes
currentThreadAllocatedBytesPerView = float allocatedBytes / float frames.Length
processGcCollections = collections; areaNodes = areaNodes; points = points |} |]
let distribution values =
let sorted = Array.sort values
{| minimum = sorted[0]; median = (sorted[9] + sorted[10]) / 2.; maximum = sorted[19] |}
let definition = "GAME-PORTAL-01.P3;Presentation.view;areas=2;entities=2;setupTicks=5;pairs=4;alpha=0,0.5,1;viewsPerCycle=12;warmupCycles=5;samples=20;cyclesPerSample=100;maxAreaNodesPerView=2;pointsPerView=2;noSimulationDuringMeasurement;v1"
let assembly = typeof<Presentation.Frame>.Assembly.Location
let result =
{| schema = "fsgg.portal.presentation-smoke/1"; originalItem = "GAME-PORTAL-01.P3"
declaredSourceRevision = sourceRevision; assemblySha256 = File.ReadAllBytes assembly |> sha256
workloadDefinition = definition; workloadSha256 = Encoding.UTF8.GetBytes definition |> sha256
historicalPreEditBaseline = "unknown"; verdict = "current-smoke-observed-no-timing-threshold"
scope = "Actual Presentation.view interpolation+scene projection; loop, frame storage and counter overhead included. Setup, validation and JSON outside timing. No physics, raster, GPU or product-adoption timing."
counterScope = "Elapsed stopwatch; process CPU and GC collections; current synchronous measurement-thread allocations. Process CPU has coarse resolution and may include runtime threads. No overhead subtraction."
runtime = Environment.Version.ToString (); os = System.Runtime.InteropServices.RuntimeInformation.OSDescription
architecture = System.Runtime.InteropServices.RuntimeInformation.ProcessArchitecture.ToString ()
processorCount = Environment.ProcessorCount; stopwatchFrequency = Stopwatch.Frequency
stopwatchHighResolution = Stopwatch.IsHighResolution; warmupCycles = 5; samples = samples
measuredViews = samples |> Array.sumBy _.views
elapsedNanosecondsPerView = samples |> Array.map _.elapsedNanosecondsPerView |> distribution
processCpuNanosecondsPerView = samples |> Array.map _.processCpuNanosecondsPerView |> distribution
currentThreadAllocatedBytesPerView = samples |> Array.map _.currentThreadAllocatedBytesPerView |> distribution |}
JsonSerializer.Serialize(result, JsonSerializerOptions(WriteIndented = true)) |> printfn "%s"
0

[<EntryPoint>]
let main argv =
if argv = [| "--presentation" |] then
Expand All @@ -18,8 +106,16 @@ let main argv =
printfn "PASS portal presentation: ticks=%d frames=%d areaNodes=%d points=%d alpha=0,0.5,1 local-metres=+Y-up"
snapshots.Length frames.Length (counts |> List.sumBy fst) (counts |> List.sumBy snd)
0
elif argv = [| "--presentation-performance" |] then
match Environment.GetEnvironmentVariable "FSGG_PORTAL_SOURCE_REVISION" |> Option.ofObj with
| Some revision when revision.Length = 40 && revision |> Seq.forall (fun c -> c >= '0' && c <= '9' || c >= 'a' && c <= 'f') ->
qualifyPerformance revision
| _ ->
eprintfn "--presentation-performance requires FSGG_PORTAL_SOURCE_REVISION (exact lowercase 40-hex; declared externally, not inferred)."
2
elif argv.Length <> 0 then
eprintfn "Usage: Box2D.Portals [--presentation]"
eprintfn "Performance smoke: --presentation-performance (requires FSGG_PORTAL_SOURCE_REVISION)."
2
else
qualifyDefault ()
Loading