17
0
mirror of https://github.com/MaSzyna-EU07/maszyna.git synced 2026-10-17 14:29:20 +02:00
Files
maszyna/scene/scenerybinary_format.md
maj00r 707b4de2f4 Editor checkpoint: turnout domain service, web->editor rename, Id-1 templates
Checkpoint of the editor branch so work isn't carried loose across a branch
switch. Builds.

- maj0sted turnout geometry moved into the domain: domain::lay_turnout lays the
  diverging path (pre-blade lead, blade at beta, arc of alfa-beta, frog rail) and
  solve_junction is a thin adapter onto it.
- the maj0sted web/ tree renamed to editor/ (namespace and paths) - there is no
  WASM here, it's plain native C++ the editor calls.
- click places the switch at its start (początek rozjazdu); the branch's leaving
  straight is locked tangent to the frog; switches can be slid along the through
  track and flipped L/P.
- Polish turnout templates carry catalogue length (Id-1-consistent lengths), plus
  blade-angle and pre-blade fields (left 0 until the catalogue figures are in).
- TURNOUT_SPEC.md: the Bezier-based runtime/model contract.
- also folds in other pending branch changes (scenery, tests) to leave a clean tree.
2026-08-27 19:36:55 +02:00

229 lines
14 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Binarny format scenerii („eu7") — specyfikacja zapisu na dysku
Źródło autorytatywne: [`scene/scenerybinary.h`](scenerybinary.h) i
[`scene/scenerybinary.cpp`](scenerybinary.cpp). Ten dokument opisuje format tak, jak kod go
aktualnie czyta i zapisuje (wersja na dysku: **10**). Jeśli kiedykolwiek się rozjadą,
rozstrzyga kod — wtedy zaktualizuj ten plik.
## 1. Cel
Format binarny zastępuje stare formaty tekstowe (`.scn` / `.scm` / `.inc` / `.ctr`) oraz dawny
blob terenu `.sbt`. To **przezroczysty, modularny cache**: każdy tekstowy plik scenerii
kompiluje się do dokładnie jednego binarnego „twina", konsumowanego na warstwie plikowej
`cParser`, więc deserializer scenerii nigdy nie widzi formatu. Twin przechowuje własny, w pełni
rozwinięty strumień tokenów pliku źródłowego (komentarze usunięte, liczby otypowane, stringi
zinternowane) i zachowuje dyrektywy `include` jako referencje — twin jednego pliku wskazuje na
twiny plików, które tamten dołącza.
Cele projektowe: małe pliki, **odczyt strumieniowy z bufora** (cały twin wczytany raz, potem
parsowany wskaźnikiem, a tokeny-stringi serwowane jako widoki — bez wywołań strumienia
per-bajt, bez materializacji pośredniej), równoległy/offline'owy baking oraz **progresywne
ładowanie** (najpierw infrastruktura, żeby gra mogła ruszyć, potem strumieniowanie węzłów
wizualnych w kolejności odległości od kamery).
## 2. Nazewnictwo plików (mapowanie twina)
Jeden plik źródłowy → jeden twin; rozszerzenie binarne wynika z rodzaju źródła
(`scenerybinary_extension_for()`):
| źródło | twin | `scenery_file_kind` |
|---------|---------|---------------------|
| `.scn` | `.scnb` | `scn` (0) |
| `.inc` | `.incb` | `inc` (1) |
| `.scm` | `.scmb` | `scm` (2) |
| `.ctr` | `.ctrb` | `scn` (0)¹ |
¹ `.ctr` dostaje własne rozszerzenie `.ctrb`, ale jest tokenizowany jako rodzaj `scn` (enum
`kind` nie ma osobnej wartości dla `ctr`).
Twin jest **odtwarzany tylko, jeśli jest co najmniej tak nowy jak źródło tekstowe** (porównanie
czasu modyfikacji); edycja pliku scenerii wymusza rekompilację zamiast odtworzenia
nieaktualnego twina.
## 3. Ogólny układ pliku
```
+-----------------------------------------------------------+
| Nagłówek |
| magic : uint32 LE = MAKE_ID4('e','u','7',10) | 4 bajty
| kind : uint8 = scenery_file_kind | 1 bajt
| flags : uint32 LE (bit0 = infra-relevant) | 4 bajty
+-----------------------------------------------------------+
| Tablica stringów |
| count : varint |
| powtórz count razy: |
| length : varint |
| bytes : <length> surowych bajtów (bez NUL) |
+-----------------------------------------------------------+
| Strumień wpisów |
| wpisy jeden po drugim aż do końca pliku |
+-----------------------------------------------------------+
```
* **magic** — `"eu7"` plus jednobajtowa wersja (obecnie `10`), little-endian przez `MAKE_ID4`.
Reader odrzuca każdy bufor, którego magic/wersja nie zgadza się dokładnie (starsze twiny są
rekompilowane, nie odczytywane błędnie). Minimalny poprawny plik to 9 bajtów (nagłówek).
* **kind** — z jakiego formatu tekstowego ten twin został skompilowany (tabela wyżej).
* **flags bit0 (`infra-relevant`)** — ustawiony, jeśli plik ma co najmniej jeden węzeł
infrastruktury lub jakikolwiek `include`. Czysto wizualny liść (np. `.incb` z florą: same
kształty + dyrektywy transformacji) ma go wyzerowany, więc pass infrastrukturalny może w ogóle
nie otwierać pliku, zamiast przechodzić go po to, żeby odrzucić jego treść wizualną.
* **tablica stringów** — każdy odrębny tekst tokenu jest internowany raz i referowany dalej
przez swój zerobazowy indeks. Stringi mają prefiks długości (bez terminatora NUL), więc reader
może serwować `std::string_view` wprost w bufor.
## 4. Kodowania prymitywów
* **varint** — LEB128 bez znaku: 7 bitów danych na bajt, najwyższy bit = „kolejny bajt
następuje". Jeden bajt obejmuje wartości < 128 — to przypadek typowy dla indeksów tablicy
stringów.
* **zig-zag** — liczby ze znakiem są mapowane na bez znaku przed varintem
(`(n << 1) ^ (n >> 63)`), więc małe wartości ujemne pozostają krótkie.
* **f32 / f64** — IEEE-754, little-endian (4 / 8 bajtów), zgodnie z `sn_utils`.
Wszystkie skalary wielobajtowe są little-endian.
## 5. Strumień wpisów
Każdy wpis zaczyna się od **head varint**. Jego dolne 3 bity to tag (`TAG_MASK = 0x7`); pozostałe
górne bity (`head >> 3`) niosą ładunek wpisów-tokenów (indeks w tablicy stringów). Przypadek
typowy — token — kosztuje więc tylko ten jeden varint.
| wartość tagu | nazwa | ładunek po head |
|--------------|-----------|-------------------------------------------------------------------|
| 0 | `TOKEN` | brak — `head >> 3` to indeks w tablicy stringów. Małymi literami per-konsument przy odtwarzaniu. |
| 1 | `INCLUDE` | `varint(fileexpr_count)` + tyle `varint(string_idx)`, potem `varint(param_count)` + tyle `varint(string_idx)`. |
| 2 | `INT` | jeden zig-zag varint (wartość całkowita ze znakiem). |
| 3 | `F32` | 4 bajty, float little-endian. |
| 4 | `F64` | 8 bajtów, double little-endian. |
| 5 | `QTOKEN` | brak — `head >> 3` to indeks w tablicy stringów. **Token cytowany: case zachowany dosłownie** przy odtwarzaniu (nigdy nie zmniejszany). |
| 6 | `NODE` | marker węzła — patrz §6. Po nim następuje ciało węzła. |
Logiczne typy wpisów widziane przez konsumenta (`scenery_entry_type`): `token`, `qtoken`,
`number` (dowolny z INT/F32/F64), `include`. Podział INT/F32/F64 to szczegół kodowania ukryty
wewnątrz readera/writera.
### Kodowanie liczb (`write_number`)
Token liczbowy zapisywany jest w najbardziej zwartej formie, która wystarczająco dobrze
round-trip'uje:
1. Wartość **całkowita** w zakresie ±2⁵³ (`±9007199254740992`) → `INT` (zig-zag varint). Pokrywa
wiele małych liczb w jednym–dwóch bajtach.
2. W przeciwnym razie **`F32`**, jeśli float jest skończony i reprezentuje wartość albo dokładnie,
albo z błędem względnym ≤ `1e-6` (`|float(v) − v| ≤ 1e-6·|v|`).
3. W przeciwnym razie pełny **`F64`**.
### Wpisy include
Include zapisuje **dosłowne wyrażenie nazwy pliku** (pojedynczy token albo zbiór losowy, np.
`[ a.inc b.inc ]` — z tokenami nawiasów) plus listę parametrów, wszystko jako indeksy
zinternowanych stringów. Przy odtwarzaniu wyrażenie jest re-ewaluowane (z ponownym losowaniem
zbioru losowego), a plik-dziecko wchodzony przezroczyście; własne tokeny dziecka pochodzą z jego
twina (lub tekstu). To właśnie zachowuje `cParser::Name()`, grupowanie węzłów per-`.inc` oraz
substytucję parametrów `(pN)` dokładnie jak ścieżka tekstowa.
## 6. Markery węzłów (progresywne ładowanie)
Każdy **węzeł najwyższego poziomu** (blok `node … end<typ>`; węzły nigdy nie są zagnieżdżone)
jest opakowany w marker `NODE`, żeby reader mógł serwować lub pomijać cały węzeł per-pass.
Układ markera:
```
varint(TAG_NODE) # head, tag = 6
varint(class) # NODECLASS_* (poniżej)
varint(byte_span) # długość w bajtach buforowanego ciała następującego po markerze
[ f32 X, f32 Y, f32 Z, f32 Range ] # TYLKO gdy class == NODECLASS_VISUAL_POS (4 floaty)
<body> # `byte_span` bajtów: własne wpisy węzła (tokeny/liczby/...)
```
Klasy węzłów (`NODECLASS_*`):
| wartość | nazwa | znaczenie |
|---------|----------------|------------------------------------------------------------------------|
| 0 | `INFRA` | węzeł infrastruktury (tor, trakcja, memcell, event, dźwięk, …). |
| 1 | `VISUAL` | węzeł wizualny bez pozycji w markerze (kształty / starsze twiny). |
| 2 | `VISUAL_POS` | wizualny węzeł modelu, którego marker niesie pozycję lokalną **oraz zasięg widoczności** — 4 f32: `X, Y, Z, Range` (`Range` = `range_max`, `-1` = nieograniczony). |
> Uwaga: komentarz w kodzie przy `NODECLASS_VISUAL_POS` wciąż mówi „3 f32"; writer realnie
> emituje **4** floaty (X, Y, Z, Range). Na dysku jest układ 4-floatowy powyżej.
`Range`/pozycja pozwalają ładowaniu wizualnemu sterowanemu odległością od kamery („ring")
**przetestować dystans i pominąć węzeł w O(1)** — bez dekodowania żadnego z jego tokenów — oraz
budować eagerly modele dalekie, ale o dużym zasięgu (range poza promieniem strumieniowania),
zamiast porzucać je na dystansie. Kształty (triangles/lines) nie mają pozycji w markerze i
spadają do testu sekcyjnego w deserializerze.
## 7. Passy ładowania (`scenery_load_pass`)
Readerowi mówi się, którą klasę węzłów serwować; `next()` pomija markery węzłów i ciała węzłów
spoza passu (przesuwając kursor o `byte_span` w O(1)), aż dojdzie do wpisu do zaserwowania.
**Wpisy nie-węzłowe (dyrektywy, include'y) są serwowane zawsze**, w każdym passie, by utrzymać
spójny stan transformacji/grup.
| pass | serwuje |
|------------------|-------------------------------------------------------------------|
| `all` (0) | wszystko (ładowanie jednoprzebiegowe, == zachowanie sprzed v6). |
| `infrastructure` | dyrektywy/include + węzły `INFRA`; węzły wizualne pominięte. |
| `visual` (2) | dyrektywy/include + węzły `VISUAL`/`VISUAL_POS`; infra pominięta. |
Ten sam twin jest otwierany raz, potem przechodzony ponownie: najpierw infrastruktura eagerly
(gra może ruszyć), potem wizuale strumieniowane po sekcjach kamery.
## 8. Powierzchnia API readera (semantyka)
`scenery_binary_reader` (bufor musi przeżyć reader):
* `open(string_view)` — waliduje nagłówek, indeksuje tablicę stringów, ustawia na pierwszym
wpisie.
* `set_pass(pass)` / `next(out)` — strumieniuje kolejny serwowany wpis (§7).
* `node_position(x, y, z, range)` — pozycja lokalna + `range_max` aktualnie serwowanego węzła,
jeśli marker je niósł (`VISUAL_POS`); w przeciwnym razie false.
* `skip_to_node_end()` — przeskakuje kursor na koniec aktualnie serwowanego węzła (O(1)), by
porzucić węzeł po podejrzeniu tylko jego pierwszych wpisów (ring skip kamery).
* `node_offset()` / `seek_node(offset)` — bajtowy offset markera bieżącego węzła oraz
repozycjonowanie tam, żeby kolejne `next()` zaserwowało ten węzeł (odbudowa jednego węzła na
żądanie bez re-skanowania twina).
* `infra_relevant()` — flaga z nagłówka (§3); pozwala passowi infrastrukturalnemu w ogóle nie
otwierać czysto wizualnych liści.
* `progress()` — ułamek skonsumowanej sekcji wpisów (0..100), dla paska ładowania.
Odczyt jest **kontrolowany pod kątem granic**: przy przekroczeniu kursor parkuje na końcu i
zwracane jest zero, więc obcięty twin dekoduje się do pustki zamiast czytać poza bufor.
## 9. Wersjonowanie
`SCENERYBINARY_MAGIC` osadza wersję w 4. bajcie magic (obecnie `10`). Podbicie unieważnia
wszystkie starsze twiny (nie przechodzą `open()` i są rekompilowane) — tak bezpiecznie wdraża
się zmiany formatu. Skrócona historia (szczegóły w nagłówku):
* **v4** — reader strumieniowy z bufora, spakowane tagi wpisów, liczby całkowite jako zig-zag
varint.
* **v5** — tokeny w oryginalnym case + flaga cytowania, więc baking jest niezależny od gramatyki
(umożliwia headless/standalone baker).
* **v6** — węzły najwyższego poziomu opakowane w marker (klasa + byte span) → progresywne
ładowanie.
* **v7** — marker modelu wizualnego niesie też pozycję lokalną modelu.
* **v8+** — poprawka terminatora eventlauncher; marker niesie też zasięg widoczności modelu
(`range_max`), dając obecny 4-floatowy ładunek `VISUAL_POS`. Aktualna wersja na dysku: **10**.
## 10. Baking
Serializacja i zapis twina to praca samodzielna, zdejmowana ze ścieżki budowy sceny:
* **Asynchronicznie** (`scenerybinary_write_async` / `scenerybinary_wait_all`) — podczas
normalnego ładowania tekstowego gotowe writery trafiają do małej puli wątków, więc I/O nakłada
się na resztę budowy sceny; wyniki logowane są na wątku głównym po zakończeniu ładowania.
* **Headless** (`scenerybinary_bake_headless`, tryb linii poleceń `-bake`) — tokenizuje
scenariusz i, rekurencyjnie, każdy dołączany plik, kompilując każdy twin **bez sceny, renderera
i okna**. Graf plików odkrywany jest przez tokenizację każdego pliku w izolacji (include'y
zapisane, ale nieotwierane), a każdy twin kompilowany na wątku roboczym; każdy plik bakowany
jest dokładnie raz (deduplikacja), więc zapisy twinów nigdy się nie ścigają. Świeże twiny są
nietknięte; rekompilowane są tylko brakujące/nieaktualne.
## 11. Kto używa maszynerii odtwarzania
Tylko deserializer scenerii (`simulation/simulationstateserializer.cpp`) steruje API
replay/pass/streaming binarnego twina. Każdy inny konsument `cParser` (modele, eventy, pojazdy,
audio, config, …) korzysta ze zwykłego tokenizera tekstu i nie wie nic o formacie binarnym.