Клиент: начальный импорт кодовой базы

Основа — Horizon (MIT). История апстрима не переносится: этот репозиторий ведёт свою.
This commit is contained in:
SashegDev
2026-10-29 22:04:28 +03:00
commit 874b6f20c2
3414 changed files with 1071219 additions and 0 deletions
@@ -0,0 +1,56 @@
# Lighting worker result packaging
`light_chunk` now returns only `Vec<Light>`. `apply_lighting` moves each light
into the current section through `Arc::make_mut`, preserving current block and
biome edits and snapshots held by other workers. Streaming still rejects stale
sequences before applying results and replays edits after applying valid light.
All direct examples use the same application function.
## Reproduce
```sh
CARGO_INCREMENTAL=0 CARGO_TARGET_DIR=/home/bob/Documents/horizon/target cargo test -p horizon-client --lib --profile fast benchmark_worker_light_results -- --ignored --nocapture
```
The ignored test invokes the production `pack_lighting` and `apply_lighting`
functions and compares them to original main's packaging/application loops.
It checks every resulting block, biome, and light value before timing. Each
measurement uses 500 columns per batch, one discarded warm-up batch, and ten
measured batches with alternating baseline/optimized order and `black_box`.
The reported median is the upper middle of the ten samples. Both implementations
receive identical precomputed propagation arrays. The fixture contains 24
sections with 16-state block palettes, four-biome palettes, and existing full
light arrays. A second case retains a fresh snapshot each iteration to force
copy-on-write during application. Fixtures are synthetic, not loaded saves.
AMD Ryzen 5 5600X, Linux x86_64, rustc 1.99.0, optimized `fast` profile,
nonincremental, no LTO:
| Packaging + application | Original main | New | Speedup |
| --- | ---: | ---: | ---: |
| Sections uniquely owned | 63.948 us/column | 44.397 us/column | 1.44x |
| Snapshot retained | 65.332 us/column | 53.025 us/column | 1.23x |
These measurements exclude propagation, region snapshot creation, outer chunk
copy-on-write, channels, replay, and remeshing. They establish savings in the
actual worker output/application paths, not a whole lighting runtime or FPS gain.
## Copy and allocation scope
`Section::heap_bytes` reports 149,760 bytes of fixture payload per column.
Original packaging clones that payload and allocates 24 section Arcs. Original
application clones the current section payload, allocates another 24 section
Arcs, and clones 98,304 bytes of returned light. New packaging clones no section
payload and allocates no section Arcs. New application moves the returned light;
with unique sections it clones no payload and allocates no section Arcs. With a
retained snapshot it still copies the current section payload and allocates 24
Arcs, as required to preserve the snapshot. New light arrays and the result Vec
still allocate. These are structural copy/allocation observations, not global
allocator instrumentation; byte figures exclude Arc headers, Vec headers, and
allocator overhead.
Focused tests cover moved light buffer identity, current block/biome edits,
retained snapshots, forced replay versus fresh full relighting, empty chunks,
missing neighbours, no-sky dimensions, light attenuation, occlusion, and
incremental/full equivalence. The stale-sequence guard and missing-current-chunk
handling in streaming are unchanged.
@@ -0,0 +1,73 @@
# Paletted container access
The `unpack`/`get` cell reader and `BlockSection::get`/`set` are hot in the
chunk workers: light and mesh workers unpack a section's 4,096 cells before
propagating or meshing, and world-generation workers write one block at a time
through `BlockSection::set`.
The write path resolved the old cell and probed the palette three times; the
bulk readers dispatched the palette per cell; and the storage layer revalidated
indices and values a section had already checked. All three are addressed.
## Changes
`BlockSection::set` (`crates/horizon-world/src/chunk.rs`) previously resolved the
old cell through `get` and then probed the section's palette map three times for
one non-trivial write: once to key the old state by id (`local_ids[&old]`), once
to test the incoming state (`contains_key`), and once to find it (`get`). The
slot at the written index already *is* the old state's id, so the old state is
now read straight from storage, and the incoming state is resolved with a single
map probe. The palette-reuse, free-slot and growth paths are unchanged, so the
encoded palette, counts, free list and NBT output stay identical.
`BlockSection::get` and `set` validate the section index and the state id once,
then read and write through `PackedStorage::read`/`write`
(`crates/horizon-core/src/storage.rs`). These skip the storage-level bounds and
value checks and the `Result` that `PackedStorage::get`/`set` keep for external
callers: a section index is below 4,096 and a palette id always fits the width
`bits_for_palette` chose. Both new methods `debug_assert` their contract and are
used for the hot read, the hot write and the palette-growth re-encode.
`PalettedContainer::unpack` and `unpack_u16`
(`crates/horizon-core/src/paletted.rs`) matched on the palette variant inside the
per-cell loop. `Single`, `List` and `Global` are now dispatched once before the
loop, leaving each arm a branch-free indexed loop.
No allocation, layout or wire format changes.
## Reproduce
```sh
cargo test -p horizon-world --lib --profile fast benchmark_section_get_set -- --ignored --nocapture
cargo test -p horizon-core --lib --profile fast benchmark_unpack -- --ignored --nocapture
```
The ignored tests compare the new code against copies of the pre-optimization
implementations, kept in the test modules (`baseline_get`/`baseline_set`,
`baseline_unpack`). They check every resulting palette, count, free list, cell
and NBT before timing. Rounds alternate baseline/optimized order with `black_box`
and report the median and best nanosecond- or microsecond-per-operation; the best
round is the estimate of uninterrupted CPU time on a contended machine.
`benchmark_section_get_set` uses a 32-state registry and 50,000 random writes and
reads per round over a section whose palette grows through the 4- and 5-bit
widths (24 distinct states); 51 rounds. `benchmark_unpack` fills a 4,096-entry
list palette (12 states) and the global palette, 2,000 calls per round, 25
rounds.
AMD Ryzen 5 5600X, Linux x86_64, rustc 1.99.0, optimized `fast` profile,
nonincremental, no LTO:
| Operation | Original main | New | Speedup |
| --- | ---: | ---: | ---: |
| `BlockSection::set` | 13.15 ns/write | 9.12 ns/write | 1.44x |
| `BlockSection::get` | 2.33 ns/read | 1.45 ns/read | 1.61x |
| `unpack`, list palette | 1.910 us/call | 1.834 us/call | 1.04x |
| `unpack`, global palette | 2.270 us/call | 2.179 us/call | 1.04x |
The distinct-state differential test
`chunk::tests::set_matches_baseline_under_random_edits` drives the optimized
`set` and `get` and the originals over the same 50,000-edit streams for 1, 2, 16,
17, 24 and 64 distinct states, including periodic whole-section collapses that
clear the storage and reclaim palette slots, and asserts equal results, bits,
palette, counts, free list, `local_ids` and NBT throughout.
+61
View File
@@ -0,0 +1,61 @@
# Progressive world resources (#712)
Stage 3a changes shared resource storage and accessors. Pack resources keep their existing per-game ownership for pack reloads. Stage 3b publishes core before the simulation tail, builds the real renderer using the previous device after releasing its surface, and gates backdrop streaming on that renderer. Stage 3c queues an early Play or Connect action and draws the existing level-loading progress screen while polling the worker each event-loop turn. Close and resize events continue to work. After the worker completes, the queued local/server action resumes; integrated servers receive the resource handle only after that join. The synchronous harness and dedicated path returns only fully loaded resources.
Startup logs distinguish `core-ready`, `world renderer ready`, `starting title backdrop`, and `rest-ready`. Opt-in performance summaries carry `to_core_ms` and `to_rest_ms` separately from the existing UI-driven `to_title_ms`. Loader failures and panics propagate to the client; a tail taking more than 120 seconds fails rather than waiting indefinitely.
## Stage 3d: interactive menu before core
An ordinary launch now owns a `StartupMenu` inside `Loading`. The first menu atlas and panorama message makes the existing title screens interactive; assets, facts, biomes, pack tables, collision and motion continue on the resource worker. Pack-overlay and mod-content preparation also run on that worker. The live landscape still starts only after the core renderer exists.
World browsing, world creation, server address entry and options use the existing title/options widgets. Play and Connect queue a `PendingWorldEntry`; the progress screen covers both the core and tail waits. Escape cancels the queued entry in either phase. Close and resize continue to work. The handoff transfers the title object, options screen, settings, cursor, clipboard selection, timing session and pending action instead of rebuilding the user's screen.
Pack changes during core loading request another loader generation. The old core and tail finish before replacement begins so cache writes and tail registry initialization cannot overlap. Each generation has its own message channel and progress counter. Language/GUI metadata publication stays on the event-loop thread, using the current language selection rather than the worker's stale settings snapshot. Pack changes after the core handoff also wait for the tail before touching mob metadata.
The menu still needs its own artwork and renderer before interaction. The core handoff still builds the full renderer and starts audio on the main thread; menu click sounds and controller navigation start with `Game`. This stage does not change synchronous harness/dedicated startup or introduce a render-facts export.
Regression coverage publishes menu artwork while a fake core worker is blocked, edits a world draft, replaces fallback artwork without losing it, queues entry and draws progress, and checks that loader failure reaches the app. Additional checks cover server identity and cancelled-entry timing.
## CPU preparation before handoff
The resource loader now prepares the game's entity models, item-frame models,
painting variants and creative inventory, plus the block-sprite and world-border
mip chains, before publishing `Loaded`. All startup entity models share one parsed
`entity-models.json` document. Each loader generation owns its prepared data;
restarting after a pack change discards that generation's models and uploads.
Fallible preparation finishes before the world-rest worker starts, so preparation
failure cannot leave a detached tail writing into the next loader generation.
The event-loop handoff consumes the prepared game data and mip chains. GPU
pipeline creation, texture uploads, surface operations and audio initialization
still run on the main thread. Upload buffers are released after construction.
The handoff uses the current menu settings and title object directly, avoiding
another options-file read, theme-preference load and temporary title construction.
Synchronous harnesses and integrated servers prepare their game data inline.
Logs now report worker CPU preparation time and the handoff's total, renderer and
`start_game` times separately. The reported 245 ms handoff (103 ms renderer,
118 ms `start_game`) predates this change; the new timings should be compared in
an otherwise identical build and GPU environment.
## Render facts assessment
`MeshTables::new` reads names, registry properties, class membership, collision shape flags, fluid and render properties from `BlockFacts`. `LightTables::new/load` reads emission, dampening, occlusion and shape-occlusion flags; geometry comes from `occlusion.nbt`. Neither table needs recipes, item data, AI or the other tail tables.
A render subset is possible, but the current facts decoder also validates registry order and builds the gameplay projection from the same document. Meshing additionally depends on class chains from `block-behaviours.json`. Avoiding the 54 MB read requires an exported render-facts artifact (and its prepare/cache/version checks), or another decoder plus a second full read at world entry. This is beyond a cheap startup ownership change. Stage 4 can add that artifact and compare the resulting mesh/light tables against the full loader before moving facts out of core.
## Measurement
Measurements on this machine are warm runs with `HORIZON_NO_AUDIO=1`. Other compilation and parity jobs were running, so elapsed times are diagnostic rather than a controlled benchmark.
Before phasing (3a, `42d65465`), `--offscreen --frames 3 --smoke-report` completed successfully with peak RSS 762,536 KiB. The report contained 3 frames, no chunks, no generated or meshed sections, no player, and no error. This is the synchronous harness path; the progressive path is measured separately after 3c.
After phasing, the same warm three-frame synchronous smoke completed successfully with peak RSS 774,080 KiB, up 11,544 KiB (1.5%) from the 3a measurement. Both reports had 3 frames, no chunks, no generated or meshed sections, no player, and no error. Elapsed time is intentionally excluded from the comparison because the machine was running concurrent builds.
The progressive checks used an overlay of the prepared resources with `mob-data.json` replaced by a FIFO. This holds WorldRest without a production delay hook. Core and the real renderer became ready, Options → Video opened, and the backdrop logged `title backdrop ready; world rest ready: false` before the FIFO was released. In an unblocked Horizon launch, core was ready at 4,776 ms, the renderer at 5,260 ms, and rest at 6,282 ms under compilation load. Backdrop generation and fade-in still take their own time; these marks measure bundle readiness separately.
A direct singleplayer launch while the FIFO was held displayed `Loading items` and handled a window resize. Releasing the FIFO resumed entry; the run exited cleanly with a player, 85 chunks, 529 meshed sections and no error. That delayed-entry run peaked at 1,142,560 KiB, including the loaded world. Integrated-server entry also joined after release and rendered a world. Dedicated startup reached `Done`, handled `list` and `stop`, saved and exited with status 0 without constructing a renderer.
The full parity selection against `origin/main` also includes the unmerged Stage 2 ancestors (313 harnesses). The local run was bounded at 30 minutes; its log recorded passes for `active_effects`, `active_effect_tick` and `anchor`. The remainder needs CI or a longer dedicated run. The client Rust tests, focused startup probes and memory measurements are separate from that incomplete parity run.
Progressive-title RSS was measured separately with audio disabled, render distance 4 and a 30 FPS cap, using separate temporary game directories. The first 30-second pair measured 1,055,132 KiB before and 1,721,772 KiB after. A warm 60-second repeat in reverse order measured 1,031,948 KiB before and 1,145,776 KiB after: an increase of 113,828 KiB (about 111 MiB, 11%). These launches ran during compilation and did not advance through startup/streaming identically. They establish a memory cost to investigate, not a claim of RSS parity; a quiet-machine cold/warm comparison remains needed before merging the stack.