Repository navigation
Art pass slices 1-2: visuals schema, draw-call counter, inspect_gltf and vendored KayKit/Inter - #52
Merged
Merged
Conversation
data/visuals.ron gains a required buildings map (every kind is Primitive(Cuboid), the M3b box, so the look is unchanged) and empty team_swatches and terrain tables. Visual::Scene gains nodes, parts, team and fallback; Visuals gains the optional sites, rubble, seat_flag, terrain and team_swatches tables. Visuals::validate checks every kind and key, the Scene path, scale, yaw, node and part rules, building and site looks, team swatches, rubble, flag and terrain ranges, and the 1.4x silhouette rule over the live file, each naming its field, with negative tests. sim-cli data-check checks that every Scene file exists under --assets (default <data>/../assets), --no-assets skips it, and the OK line ends with visuals=units:8 buildings:8 terrain:<n> models=<n>. The presenters read the schema: a unit Scene draws its fallback, a building is the box for Primitive(Cuboid) and for a Scene. The present.rs look tests measure the M4b looks frozen as M4B_LOOKS, so a visuals.ron edit never fails a Rust test. visuals.ron is outside rules_hash (ADR 0003): no rules_version bump, no fixture change.
art_bench (seed 40, standard start on plains_1v1) is the draw-call and frame-time scene of the art pass: P0 places eleven complete buildings and three sites around its Seat and P1 the mirrored eleven, so each side has twelve complete buildings; each side spawns 195 units (60 Skirmishers, 40 Shieldbearers, 40 Bowmen, 24 Outriders, 10 Mangonels, 6 Wains, 10 Yeomen, 5 Scribes) in groups of ten east of P0's Town. P0's start Yeomen build two of the sites from tick 2, the armed units attack-move at each other at tick 20 and three of P0's buildings lose a third of their hit points at tick 30. It is a scenario, not a fixture; NAMES has 17 entries. The test checks the Seats, the 2 x 12 complete buildings and 3 sites with their ids and mirrored tiles, the 400 units with their ids, kinds and spawn areas, and that no command is rejected through the damage beat.
draw_stats.rs counts the draw calls each frame in the render world from Bevy 0.19.1's public phase data: one per indirect-parameters entry, the bins and unbatchable entities of views with NoIndirectDrawing, and the sorted Transparent3d, Transmissive3d and TransparentUi items walked as render_range walks them. The dev panel shows `draw calls <last> (max <max>) / 500 ind dir sorted` (amber from 500); with --draw-stats (always in dev builds) the exit prints `frame_stats: draws ... mode=` (and fight_max in the fight window) and `frame_stats: triangles ...`. EONMARK_WGPU_DISABLE turns wgpu features off for the trace cross-check, scripts/draw_trace_xcheck.sh, which compares the counter with Bevy's detailed_trace draw lines in both modes. The game loads models from Cli::resolve_assets_dir (--assets-dir, EONMARK_ASSETS, the checkout's assets/, assets/ beside the executable) through AssetPlugin, not Bevy's crates/game/assets under cargo run. --window-size sets the logical window size and frame_stats prints the `window logical= physical= scale=` line. The debug grid is off by default (ShowGrid; --grid, G toggles it). art_bench opens at 60 m over P0's Town and the fight, its director selects every P0 unit at tick 40, and prints_fight_lines (the enemy_outpost pair and art_bench) replaces is_enemy_outpost. render_smoke.sh always passes --draw-stats, requires the draws line, prints the draws, triangles and fight lines and takes an optional sixth SECONDS argument (just render-smoke too). ui.md documents the counter; ROADMAP gains the "Art pass (pre-stream)" section with its acceptance list.
A retained gizmo is its own Transparent3d item and is never batched, so every selection ring and every attrition diamond was one draw call: with all 200 of P0's units selected, art_bench read max=241 draws with sorted_max=210 under lavapipe. Art-pass decision 11 makes both one flat annulus mesh and one unlit material each (selection::ring_mesh, 48 segments for the ring, 4 for the diamond, inner radius 0.85 of the outer; RingMesh and MarkerAssets), with NotShadowCaster, still one top-level entity per UnitId and never a child or Pickable. Any number of rings or diamonds is now one bin: the same art_bench run reads max=42 with sorted_max=10. Move and attack markers, supply rings and the annexation arcs stay gizmos. Tests: rings_and_diamonds_are_one_mesh_and_one_material_each and rings_and_diamonds_have_no_parent drive the real sync systems over capture_seat with three bleeding Bowmen; the marker-shape test now reads the diamond mesh. Docs: ui.md, ART_STYLE.md, ARCHITECTURE.md and macos-packaging.md describe the meshes; IMPLEMENTER_NOTES gains the known traps of the art pass (the asset root under cargo run, retained gizmos at one draw each, no draw-call counter in Bevy or wgpu).
BUILD_TIMES gains the lavapipe render-smoke rows the later slices compare against (boxes for buildings, the M4b looks, mesh rings): unit_looks 29 draws a frame, enemy_outpost 22 (fight_max=22), art_bench max 42 (fight_max=42, all 200 of P0's units selected; TICK 300 and SECONDS 120 hold on this VM), with their triangle and fight lines; the art_bench run before the rings became meshes (max 241, sorted_max 210); the trace cross-check, which agrees in both the culling and the preprocessing_only mode; and the warm sim suite with the art_bench test (8.0 to 9.2 s). ui.md points at the rows, and draw_trace_xcheck.sh says where XCHECK_TARGET_DIR puts the trace binary. ADR 0002's migration steps and acceptance gain the cross-check, which the script's header already pointed at: the counter reads render internals Bevy 0.20 may move. The art-pass acceptance lines AP1 and AP2 name M7's acceptance lines 3 and 4 instead of their old line numbers, which moved when the section was added.
tools/inspect_gltf is a new workspace member (members += "tools/*") for the art pass: inspect, swatches, palette, bake and check over glTF 2.0 files. It reads them with gltf 1.4.1 without its import feature (no base64 0.13 or urlencoding) and PNGs with png, and has no engine crate, no glam and no image crate: its matrix, quaternion and normal maths are hand-written and unit-tested. Cargo.lock gains only the inspect_gltf package. CI's engine-boundary loop covers it, and DEPENDENCIES.md has its rows.
tools/assets/sources.tsv lists every third-party file of the art pass with its raw.githubusercontent.com URL, sha256, licence, author and per-file page; scripts/fetch_assets.sh (just fetch-assets) checks the licence texts first (KayKit CC0 1.0, Inter OFL 1.1), then fetches and verifies the rest into a download cache and copies the vendored files into assets/. The red KayKit variants and the Adventurers characters stay in the cache for inspect_gltf swatches and bake (just art-bake; tools/assets/bake.ron has no rows until art slice 4). Vendored unmodified, 72 files, 2,985,960 bytes: KayKit Medieval Hexagon's blue buildings (the windmill too, the Croft's alternative), the neutral construction stages and rubble, flag and cart props, nature, their licence text, the Adventurers licence text, and Inter 4.1 with OFL-Inter.txt, each with its own ATTRIBUTION.md row (one row per directory is no longer offered). inspect_gltf swatches over the blue and red variants of every building, the catapult tower and the flag gives one team rect of the shared atlas, recorded as team_swatches "kaykit_hexagon"; no Scene names it yet, so nothing draws differently. ART_STYLE.md records where the files come from and the budgets measured on them, which inspect_gltf check now applies.
scripts/check_assets.sh --self-test copies only assets/ into a temporary root, injects models/unattributed.glb and requires exit 1 with that path on stderr, then exit 0 once it is removed; the allowlist is unchanged. CI's check job and just ci run it after the asset policy, then inspect_gltf check over data/visuals.ron and assets/.
The selection ring and the attrition diamond became opaque filled bands in the same plane (RING_Y). Around each diagonal their bands overlap, so under a selected unit that bleeds the two meshes z-fought: amber samples speckled the cream ring and shimmered as the unit moved. The diamond now sits 5 mm lower, at markers::ATTRITION_Y (0.025 m), so the ring is always on top. The debug grid's height becomes ground::GRID_Y, and a compile-time assertion keeps the flat layers in order: grid, diamonds, rings, the placement ghost's outline. A new test checks a selected, bleeding unit's ring is above its diamond, and the existing plane assertions expect each marker's own height.
app::run logged "assets: <dir>" before App::new, when no tracing subscriber was installed yet, so the line was dropped and no run ever recorded which asset root the game resolved. It is now logged right after DefaultPlugins (and with them LogPlugin) are added. render_smoke.sh now requires the game::app "assets:" line and that the logged directory holds ATTRIBUTION.md, and prints the root it found.
The selection panel writes one line per unit kind, but only five rows of 14 px text fit between the bottom bar's padding and the window's edge. art_bench's select-all (every P0 unit, all eight kinds) pushed three kind lines off the window in every frame after tick 40, the AP8 art-battle-60m shot included, and any box-select of six or more kinds did the same. Past MAX_KIND_LINES (5) kinds the panel now shows the first four in units.ron order and one "+N more kinds" line (new UI string info.more_kinds; strings are outside rules_hash). A test selects every kind, checks the text fits the panel and ends in the summary line, and that exactly five kinds still get a line each.
grid_is_off_by_default_and_g_toggles_it built its own schedule with a stand-in counter, so it never ran add_grid_systems or draw_grid: if draw_grid lost its run_if(grid_shown), the grid would be drawn in every build while the test stayed green. The test now adds the real GroundPlugin with the default gizmo group and reads the lines draw_grid hands to the renderer (the group's GizmoAsset, or no handle when nothing was drawn): none by default, the grid in the frame G is pressed and every frame after, none again after a second press, one toggle for a held key, and the grid from the first frame when ShowGrid(true) is inserted first, as --grid does.
CONTRIBUTING's data-only walkthrough says a test must not hard-code a number the data controls. Two new art-pass tests did: - inspect_gltf's check test expected "suggest scale_x100=95 (1.90 m footprint)" and "(fallback height 1.19 m)", worked out by hand from the Croft's 2x2 footprint and the Yeoman's radius 35; - art_bench_is_framed_on_the_town_and_the_fight expected 2 * 174 armed units, the ART_BENCH_ARMY rows of the kinds that have an attack today. Both now compute the expectation from the loaded rules: the Croft's footprint and the Yeoman's radius, and the start units of armed kinds plus each side's ART_BENCH_ARMY rows of armed kinds. Today's values are unchanged (1.90 m, 1.19 m, 348).
ATTRIBUTION.md says Modifications "none" for the 72 files vendored through tools/assets/sources.tsv, and the attribution test infers that from the file names, but nothing that runs in CI compared a vendored file with its pin: only the maintainer-only fetch hashed anything. An edited model, or an editor trimming the tabs that end lines in KayKit's licence texts, passed check_assets.sh and every test. check_assets.sh gains rule 5: when tools/assets/sources.tsv exists under the root, every row with a dest must exist under assets/ and match its sha256 (sha256sum, or shasum -a 256 on macOS); a mismatch names the file and the pin. CI and `just ci` already run the script. --self-test keeps its assets-only injection check and adds a second root with the manifest: one changed byte in a pinned file must exit 1 naming it, and the original bytes must pass. .editorconfig stops trimming whitespace and adding final newlines under assets/. ART_STYLE, ATTRIBUTION.md, CONTRIBUTING, the sources.tsv header and the attribution test's message say the bytes are now checked.
Only one synthetic test reached check_visuals, and its centred boxes on an 8x8 atlas could never trip most rules: the x/z centre tolerance, the 256-texel rule and its shipped-atlas exemption, the per-file cap, cameras, lights, skins and animations, non-indexed or NORMAL-less primitives, two base textures, an unknown team key, a swatch rect outside the texture, or the rule that only models of one rig are compared. Dropping or widening any of them left the tests green. check_flags_centre_texture_file_and_import_rules_and_rig_groups covers each with its own model: an off-centre composite along x and along z fails as a building and passes as a prop, the texture limit fails and the hexagons_medieval.png name is exempt, a 16-byte file cap trips, camera, light and rigged files are refused, and so on. testkit gains box_gltf_with to name the box's texture. The plan (section 2b.4) also has check print the facing it finds. Each line now carries facing=<side>(<share>%): the horizontal side with the most outward surface, from each triangle's winding and area, kept in four separate totals so a closed mesh does not cancel out; props print "-". check_prints_the_facing_it_finds turns a one-sided quad through the four yaws and checks a cube ties at 25%.
- "Decision 11" for the mesh rings: the only numbered art-pass decisions in the repository are the owner's, whose (11) is the grid and the extras. The ring citations in IMPLEMENTER_NOTES, ui.md, BUILD_TIMES, ARCHITECTURE, selection.rs and markers.rs now say the art-pass plan's decision 11 (rings and diamonds as shared meshes), and the ROADMAP Notes explain the two numberings. - AP9 names a "slice 1 bump" and a <slice-1> base that do not exist on this branch: ADR 0003's bump to 25 landed before slice 1. A dated note on AP9 and a Notes entry give the base, 15f1616, without rewording the line. - The open M0 [owner] line promises a grid gizmo that has been off by default since the art pass; a dated note says G or --grid shows it. - ART_STYLE said every budget was set from measurements, but the team-area floors are the plan's, and six of the eight chosen buildings and the Mangonel's catapult fall under them. ART_STYLE now says so, lists the measured shares, and describes `inspect_gltf palette` (the Inspection row promised it) and check's facing= hint; the ROADMAP Notes record the floor choice as pending with the owner before slice 3. - The swatches module doc claimed bit-for-bit geometry; it compares positions within POSITION_TOLERANCE and indices exactly.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What and why
First two slices of the pre-stream art pass (ROADMAP "Art pass (pre-stream)", owner decisions of 2026-10-26). This PR lays down the tooling and the assets. The game looks the same, apart from selection rings and attrition diamonds, which are now shared meshes. Models start being drawn in slice 3.
Slice 1: schema, counter, bench
visuals.ronschema covers buildings, sites, rubble, the Seat flag, terrain props andteam_swatches, with validators and negative tests.sim-cli data-check --assets/--no-assets--assets-dir/EONMARK_ASSETS,--window-size--gridand the G key; the grid is now off by default (owner decision 11).art_benchscenario: two full Towns and 400 units fighting.draw_stats.rs,--draw-stats, a dev panel row, exit lines) built on Bevy 0.19.1's public API only.scripts/draw_trace_xcheck.shcross-checks it.art_benchpeaks at 42 draws, down from 241, and the count no longer grows with selection size.render_smoke.shalways passes--draw-statsand takes an optional SECONDS argument.Slice 2: tooling and assets
tools/inspect_gltf, a new workspace member with no glam:inspect,swatches,palette,bake,check.tools/assets/sources.tsvpins 104 rows by sha256.scripts/fetch_assets.shchecks the licence texts first and fails closed on any pin mismatch.just fetch-assetsandjust art-bake.ATTRIBUTION.mdrow:InterVariable.ttfandOFL-Inter.txt.scripts/check_assets.sh:--self-testinjects an unattributed file and corrupts one pinned byte.inspect_gltf check data assets. The engine-boundary loop now coversinspect_gltf.Review: a four-lens review raised 16 findings; 14 were confirmed and fixed and 2 refuted. The one blocking finding was rings and diamonds z-fighting: diamonds now sit at
ATTRITION_Y = 0.025, and a compile-time assertion keeps the height order grid < diamond < ring < ghost.Determinism: no fixture or
.hashchanged, and neither didrules_version(25) orSIM_VERSION(17).visuals.ronsits outsiderules_hash(ADR 0003).No ROADMAP acceptance box is ticked here; AP1 to AP10 are ticked in later slices.
Tier
Data PR
cargo run -p sim-cli -- data-check datapasses:OK rules_version=25 rules_hash=0xfb0d0b8986fc8865 ... visuals=units:8 buildings:8 terrain:0 models=0cargo test -p simpasses: 313 passed, warm 7.4 to 9.6 srules_versionunchanged: no golden replay hash changedDocs PR
typospassesscripts/check_trademark.shpassesCode PR
just cipasses locally:RUSTDOCFLAGS="-D warnings", fmt, both clippy runs, game 196 tests,inspect_gltf23+2, rules 140, data_check 16. cargo-machete, cargo-deny and nextest are not installed locally; CI runs them.art_bench,draw_stats, the ring/diamond height test, the selection-panel cap test, the grid test throughGroundPlugin, and everyinspect_gltfcheck rule (mutation-tested).f32/f64/HashMap/HashSet/Instantin sim, rules or aicrates/game.Cargo.lockgains only theinspect_gltfpackage;gltfandpngwere already in the graph through Bevy.Asset PR
scripts/check_assets.shpasses:pins ok (72 vendored files match tools/assets/sources.tsv),ok (74 files, 3011868 bytes)ATTRIBUTION.mdrows (path, author, source URL, licence, modifications, download date)OFL-Inter.txtsits beside the fontLicensing and disclosure
How to verify
Lavapipe baselines (also in BUILD_TIMES):
Not re-run in the final round:
draw_trace_xcheck.sh(it needs a second Bevy build of about 4 GB). It passed in both GPU-preprocessing modes on the same counter code, with the counter within 29 draws of the trace over about 300 frames.Open before slice 3: team-colour floors. Six of the eight chosen buildings fall under the plan's 100‰ team-area floor, and the Mangonel's catapult nodes sit at about 25‰ against 200‰. See ROADMAP Notes and ART_STYLE "Budgets".
Generated by Claude Code