2026-06-22 01:58:51 +04:00
|
|
|
|
# V. Геометрия, материалы и рендер
|
|
|
|
|
|
|
|
|
|
|
|
Этот том описывает путь от загруженного игрового состояния до pixels в back
|
|
|
|
|
|
buffer. Renderer не решает игровые правила: он получает transforms, geometry,
|
|
|
|
|
|
материалы, свет, эффекты, камеру и список видимых объектов, затем превращает
|
|
|
|
|
|
их в упорядоченный набор draw calls и fixed-function states.
|
|
|
|
|
|
|
|
|
|
|
|
Графический pipeline FParkan держится на нескольких слоях данных:
|
|
|
|
|
|
|
|
|
|
|
|
```text
|
|
|
|
|
|
MSH node/slot/batch
|
|
|
|
|
|
-> Batch20.material_index
|
|
|
|
|
|
-> строка WEAR
|
|
|
|
|
|
-> имя MAT0
|
|
|
|
|
|
-> активная phase
|
|
|
|
|
|
-> textureName и lightmap slot
|
|
|
|
|
|
-> Texm payload
|
|
|
|
|
|
-> LegacyRenderState
|
|
|
|
|
|
-> draw item кадра
|
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
|
|
Важное практическое правило: форматы ресурсов, runtime-состояние renderer-а и
|
|
|
|
|
|
современный backend являются разными уровнями. Файл можно прочитать правильно и
|
|
|
|
|
|
всё равно получить неверный кадр из-за другой сортировки, другого mip-skip,
|
|
|
|
|
|
другой ветки material fallback или другого округления animation time.
|
|
|
|
|
|
|
|
|
|
|
|
## Контур рендера
|
|
|
|
|
|
|
|
|
|
|
|
Изображение является последней стадией длинного цикла. До renderer-а уже
|
|
|
|
|
|
накоплен ввод, рассчитан simulation step, применены отложенные операции,
|
|
|
|
|
|
обновлены animation states, выбрана camera и выставлен listener для 3D sound.
|
|
|
|
|
|
|
|
|
|
|
|
```text
|
|
|
|
|
|
system messages and input
|
|
|
|
|
|
-> simulation calculation
|
|
|
|
|
|
-> deferred object operations
|
|
|
|
|
|
-> animation and transforms
|
|
|
|
|
|
-> camera and sound listener
|
|
|
|
|
|
-> visibility and render queues
|
|
|
|
|
|
-> materials and draw passes
|
|
|
|
|
|
-> renderer completion
|
|
|
|
|
|
-> end-of-render callbacks and UI
|
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
|
|
CPU делает отбор объектов, сэмплирует animation, собирает matrices, выбирает
|
|
|
|
|
|
LOD/slot, группирует batches и готовит состояния. Графический pipeline
|
|
|
|
|
|
преобразует вершины из model space в screen space, rasterizes triangles,
|
|
|
|
|
|
проверяет depth, применяет texture stages, lighting, alpha test/blend и пишет
|
|
|
|
|
|
pixels.
|
|
|
|
|
|
|
|
|
|
|
|
Координатный путь вершины:
|
|
|
|
|
|
|
|
|
|
|
|
```text
|
|
|
|
|
|
local/model space
|
|
|
|
|
|
-> world space
|
|
|
|
|
|
-> view/camera space
|
|
|
|
|
|
-> clip space
|
|
|
|
|
|
-> normalized device coordinates
|
|
|
|
|
|
-> viewport pixels
|
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
|
|
Порядок умножения матриц и соглашение о layout должны быть едины во всём
|
|
|
|
|
|
движке. Ошибка транспонирования часто выглядит как сломанная анимация, хотя
|
|
|
|
|
|
ключи модели прочитаны верно.
|
|
|
|
|
|
|
|
|
|
|
|
## Граница Ngi32
|
|
|
|
|
|
|
|
|
|
|
|
`Ngi32.dll` является платформенной границей Iron3D-era renderer-а. Она создаёт
|
|
|
|
|
|
графический и звуковой interfaces, перечисляет устройства, хранит capability
|
|
|
|
|
|
profile, предоставляет память, часы и быстрые математические процедуры.
|
|
|
|
|
|
Высокоуровневые DLL должны обращаться к interface Ngi32, а не напрямую к
|
|
|
|
|
|
конкретному DirectDraw/Direct3D device.
|
|
|
|
|
|
|
|
|
|
|
|
`iron_3d.ini` задаёт выбранный `CURRENT_D3DCARD`. Display layer перечисляет
|
|
|
|
|
|
drivers и video modes, проверяет поддержку 3D, переводит native capabilities во
|
|
|
|
|
|
внутренний профиль и создаёт render object. `niCreate3DRender` принимает
|
|
|
|
|
|
выбранный driver/mode, window handle и flags владения, динамически получает
|
|
|
|
|
|
функции DirectDraw/Direct3D семейства 5-7 и публикует refcounted renderer.
|
|
|
|
|
|
`niGet3DRender` возвращает уже созданный объект и увеличивает число владельцев.
|
|
|
|
|
|
|
|
|
|
|
|
```text
|
|
|
|
|
|
enumerate adapters and video modes
|
|
|
|
|
|
-> choose CURRENT_D3DCARD
|
|
|
|
|
|
-> translate native capabilities
|
|
|
|
|
|
-> create DirectDraw surfaces and 3D interface
|
|
|
|
|
|
-> construct engine renderer
|
|
|
|
|
|
-> publish global refcounted pointer
|
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
|
|
Старый API работает как state machine. Перед draw подсистема terrain/shade
|
|
|
|
|
|
выбирает matrices, texture stages, filtering, depth test/write, culling, alpha
|
|
|
|
|
|
test, blending и vertex format. Современный backend может собрать это в
|
|
|
|
|
|
immutable pipeline key и реализовать через shaders, но compatibility layer
|
|
|
|
|
|
должен видеть исходную fixed-function модель.
|
|
|
|
|
|
|
|
|
|
|
|
```c
|
|
|
|
|
|
struct LegacyRenderState {
|
|
|
|
|
|
Mat4 world, view, projection;
|
|
|
|
|
|
TextureStage stages[2];
|
|
|
|
|
|
BlendMode blend;
|
|
|
|
|
|
DepthMode depth;
|
|
|
|
|
|
CullMode cull;
|
|
|
|
|
|
bool alpha_test;
|
|
|
|
|
|
uint8_t alpha_ref;
|
|
|
|
|
|
VertexFormat vertex_format;
|
|
|
|
|
|
};
|
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
|
|
Эта структура является переносимой моделью наблюдаемого контракта, а не
|
|
|
|
|
|
утверждением о точном layout оригинального объекта renderer-а.
|
|
|
|
|
|
|
|
|
|
|
|
Отдельная часть ABI -- таблица `g_FastProc`. При запуске выбираются scalar,
|
|
|
|
|
|
MMX, Katmai/SSE, 3DNow или PPro-реализации процедур, а `niGetProcAddress(index)`
|
|
|
|
|
|
возвращает pointer из изменяемой таблицы. Номер slot является частью ABI:
|
|
|
|
|
|
signature менять нельзя. Различия scalar/SIMD округления способны менять
|
|
|
|
|
|
animation sampling, culling, particles и даже gameplay-adjacent decisions.
|
|
|
|
|
|
|
|
|
|
|
|
## MSH как граф модели
|
|
|
|
|
|
|
|
|
|
|
|
`*.msh` является nested NRes, а не одной монолитной структурой. Geometry,
|
|
|
|
|
|
nodes, slots, batches, animation и служебные streams лежат в отдельных entries
|
|
|
|
|
|
и связываются по `type_id`. Физический порядок entries сохраняется для
|
|
|
|
|
|
roundtrip, но reader не должен выводить из него смысловую связь.
|
|
|
|
|
|
|
|
|
|
|
|
Карта основных entries:
|
|
|
|
|
|
|
|
|
|
|
|
```text
|
|
|
|
|
|
type 1 узлы и выбор slot, обычно stride 38
|
|
|
|
|
|
type 2 header 0x8C + slots по 68 байт
|
|
|
|
|
|
type 3 positions float3, stride 12
|
|
|
|
|
|
type 4 packed normals, stride 4
|
|
|
|
|
|
type 5 packed UV0, stride 4
|
|
|
|
|
|
type 6 index buffer, u16
|
|
|
|
|
|
type 7 triangle descriptors, stride 16
|
|
|
|
|
|
type 8 animation keys, stride 24
|
|
|
|
|
|
type 9 служебный поток модели
|
|
|
|
|
|
type 10 строки и имена узлов
|
|
|
|
|
|
type 13 draw batches, stride 20
|
|
|
|
|
|
type 15 дополнительный поток, stride 8
|
|
|
|
|
|
type 17 вспомогательные данные
|
|
|
|
|
|
type 18 редкий поток, stride 4
|
|
|
|
|
|
type 19 animation frame map, u16
|
|
|
|
|
|
type 20 редкая вспомогательная таблица
|
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
|
|
Базовый набор types стабилен для проверенных моделей Частей 1 и 2. Расширенный
|
|
|
|
|
|
вариант добавляет types 18 и 20. Редкий вариант `MTCHECK.MSH` имеет
|
|
|
|
|
|
альтернативный атрибут type 1; его payload нужно поддерживать copy-through до
|
|
|
|
|
|
закрытия layout.
|
|
|
|
|
|
|
|
|
|
|
|
### Узлы и slots
|
|
|
|
|
|
|
|
|
|
|
|
Type 1 обычно состоит из записей по 38 байт:
|
|
|
|
|
|
|
|
|
|
|
|
```c
|
|
|
|
|
|
struct Node38 {
|
|
|
|
|
|
uint16_t hdr0;
|
|
|
|
|
|
uint16_t parent_or_link;
|
|
|
|
|
|
uint16_t anim_map_start;
|
|
|
|
|
|
uint16_t fallback_key;
|
|
|
|
|
|
uint16_t slot_index[15];
|
|
|
|
|
|
};
|
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
|
|
`slot_index` образует матрицу `3 LOD x 5 groups`. Выбор выполняется как
|
|
|
|
|
|
`slot_index[lod * 5 + group]`; `0xFFFF` означает отсутствие geometry для этой
|
|
|
|
|
|
комбинации. Поле `parent_or_link` участвует в иерархии или связи узлов, но
|
|
|
|
|
|
название остаётся описательным.
|
|
|
|
|
|
|
|
|
|
|
|
Type 2 начинается с header `0x8C`, затем содержит slots по 68 байт:
|
|
|
|
|
|
|
|
|
|
|
|
```c
|
|
|
|
|
|
struct Slot68 {
|
|
|
|
|
|
uint16_t tri_start;
|
|
|
|
|
|
uint16_t tri_count;
|
|
|
|
|
|
uint16_t batch_start;
|
|
|
|
|
|
uint16_t batch_count;
|
|
|
|
|
|
float aabb_min[3];
|
|
|
|
|
|
float aabb_max[3];
|
|
|
|
|
|
float sphere_center[3];
|
|
|
|
|
|
float sphere_radius;
|
|
|
|
|
|
uint32_t opaque[5];
|
|
|
|
|
|
};
|
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
|
|
Slot связывает диапазон triangle descriptors, диапазон draw batches, AABB и
|
|
|
|
|
|
sphere bounds. AABB удобен для более точных осевых тестов, sphere -- для
|
|
|
|
|
|
быстрого отбрасывания. Последние пять слов сохраняются без интерпретации.
|
|
|
|
|
|
|
|
|
|
|
|
Обязательные проверки:
|
|
|
|
|
|
|
|
|
|
|
|
- `type 2` имеет размер не меньше `0x8C`;
|
|
|
|
|
|
- остаток после header кратен 68;
|
|
|
|
|
|
- каждый `slot_index` либо `0xFFFF`, либо меньше числа slots;
|
|
|
|
|
|
- `tri_start + tri_count` не выходит за type 7;
|
|
|
|
|
|
- `batch_start + batch_count` не выходит за type 13.
|
|
|
|
|
|
|
|
|
|
|
|
### Vertex streams, triangles и batches
|
|
|
|
|
|
|
|
|
|
|
|
Основные vertex streams:
|
|
|
|
|
|
|
|
|
|
|
|
```text
|
|
|
|
|
|
type 3: position = три float32
|
|
|
|
|
|
type 4: normal = четыре int8
|
|
|
|
|
|
type 5: UV0 = два int16
|
|
|
|
|
|
type 6: index = uint16
|
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
|
|
Normal XYZ декодируется как signed component / `127.0` с clamp в `[-1, 1]`.
|
|
|
|
|
|
Четвёртый byte normal stream не отбрасывается при roundtrip. UV декодируется
|
|
|
|
|
|
как `packed / 1024.0`. Index buffer адресует вершины относительно `base_vertex`
|
|
|
|
|
|
batch-а, поэтому проверка допустимости всегда использует
|
|
|
|
|
|
`base_vertex + index < vertex_count`.
|
|
|
|
|
|
|
|
|
|
|
|
Type 7 хранит descriptors triangles:
|
|
|
|
|
|
|
|
|
|
|
|
```c
|
|
|
|
|
|
struct TriDesc16 {
|
|
|
|
|
|
uint16_t tri_flags;
|
|
|
|
|
|
uint16_t link0;
|
|
|
|
|
|
uint16_t link1;
|
|
|
|
|
|
uint16_t link2;
|
|
|
|
|
|
int16_t nx;
|
|
|
|
|
|
int16_t ny;
|
|
|
|
|
|
int16_t nz;
|
|
|
|
|
|
uint16_t sel_packed;
|
|
|
|
|
|
};
|
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
|
|
Descriptors используются коллизией, выбором и связями triangles. `sel_packed`
|
|
|
|
|
|
содержит три двухбитовых selector-а; значение `3` преобразуется в отсутствие
|
|
|
|
|
|
ссылки (`0xFFFF`). Полная семантика links и flags не закрывается одним layout.
|
|
|
|
|
|
|
|
|
|
|
|
Type 13 задаёт draw ranges:
|
|
|
|
|
|
|
|
|
|
|
|
```c
|
|
|
|
|
|
#pragma pack(push, 1)
|
|
|
|
|
|
struct Batch20 {
|
|
|
|
|
|
uint16_t batch_flags; // +0x00
|
|
|
|
|
|
uint16_t material_index; // +0x02
|
|
|
|
|
|
uint16_t opaque4; // +0x04
|
|
|
|
|
|
uint16_t opaque6; // +0x06
|
|
|
|
|
|
uint16_t index_count; // +0x08
|
|
|
|
|
|
uint32_t index_start; // +0x0A
|
|
|
|
|
|
uint16_t opaque14; // +0x0E
|
|
|
|
|
|
uint32_t base_vertex; // +0x10
|
|
|
|
|
|
};
|
|
|
|
|
|
#pragma pack(pop)
|
|
|
|
|
|
static_assert(sizeof(Batch20) == 20);
|
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
|
|
`material_index` выбирает строку WEAR. `index_start`, `index_count` и
|
|
|
|
|
|
`base_vertex` описывают один indexed draw. Неизвестные поля могут влиять на
|
|
|
|
|
|
редкие проходы или state grouping, поэтому writer сохраняет их 1:1.
|
|
|
|
|
|
|
|
|
|
|
|
Типовой обход модели:
|
|
|
|
|
|
|
|
|
|
|
|
```c
|
|
|
|
|
|
for (Node& node : model.nodes) {
|
|
|
|
|
|
Matrix node_world = parent_world * local_transform(node);
|
|
|
|
|
|
uint16_t sid = node.slot_index[lod * 5 + group];
|
|
|
|
|
|
if (sid == 0xFFFF) continue;
|
|
|
|
|
|
|
|
|
|
|
|
Slot& slot = model.slots[sid];
|
|
|
|
|
|
if (camera.culls(transform(slot.bounds, node_world))) continue;
|
|
|
|
|
|
|
|
|
|
|
|
for (uint32_t i = 0; i < slot.batch_count; ++i) {
|
|
|
|
|
|
Batch& b = model.batches[slot.batch_start + i];
|
|
|
|
|
|
bind_wear_material(b.material_index);
|
|
|
|
|
|
draw_indexed(b.base_vertex, b.index_start, b.index_count);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
|
|
В реальном кадре между culling и draw добавляются material resolve, lightmap,
|
|
|
|
|
|
render queues и сортировка, но связи данных остаются такими.
|
|
|
|
|
|
|
|
|
|
|
|
## Иерархия и анимация
|
|
|
|
|
|
|
|
|
|
|
|
Анимация MSH меняет локальный transform узлов. Geometry streams не изменяются:
|
|
|
|
|
|
для каждого узла на кадр строится matrix из position и quaternion. Дочерний
|
|
|
|
|
|
узел наследует transform родителя, поэтому изменение корпуса переносит башню,
|
|
|
|
|
|
точки крепления и все связанные slots.
|
|
|
|
|
|
|
|
|
|
|
|
Связка состоит из:
|
|
|
|
|
|
|
|
|
|
|
|
- type 8: пул animation keys;
|
|
|
|
|
|
- type 19: карта кадров;
|
|
|
|
|
|
- `anim_map_start` и `fallback_key` в `Node38`;
|
|
|
|
|
|
- parent links, задающих порядок умножения matrices.
|
|
|
|
|
|
|
|
|
|
|
|
Ключ type 8 занимает 24 байта:
|
|
|
|
|
|
|
|
|
|
|
|
```c
|
|
|
|
|
|
struct AnimKey24 {
|
|
|
|
|
|
float position[3];
|
|
|
|
|
|
float time;
|
|
|
|
|
|
int16_t qx;
|
|
|
|
|
|
int16_t qy;
|
|
|
|
|
|
int16_t qz;
|
|
|
|
|
|
int16_t qw;
|
|
|
|
|
|
};
|
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
|
|
Quaternion components декодируются как signed value / `32767.0`. На диске
|
|
|
|
|
|
порядок полей XYZ-W, но runtime math использует логическое `[w, x, y, z]`.
|
|
|
|
|
|
Безусловная современная нормализация после чтения не добавляется без parity
|
|
|
|
|
|
проверки: она может изменить крайние кадры.
|
|
|
|
|
|
|
|
|
|
|
|
Type 19 является массивом `uint16_t`; его `attr2` задаёт общее число кадров
|
|
|
|
|
|
timeline. Для конкретного узла `anim_map_start` указывает на блок длиной
|
|
|
|
|
|
`frame_count` либо равен `0xFFFF`.
|
|
|
|
|
|
|
|
|
|
|
|
Выбор ключа:
|
|
|
|
|
|
|
|
|
|
|
|
1. вычислить frame index из времени;
|
|
|
|
|
|
2. если frame вне диапазона, взять `fallback_key`;
|
|
|
|
|
|
3. если `anim_map_start == 0xFFFF`, взять `fallback_key`;
|
|
|
|
|
|
4. иначе прочитать `map_words[anim_map_start + frame]`;
|
|
|
|
|
|
5. если значение не меньше `fallback_key`, снова использовать fallback;
|
|
|
|
|
|
6. иначе использовать mapped key и следующий key для interpolation.
|
|
|
|
|
|
|
|
|
|
|
|
Fallback возвращается без interpolation. Это защищает статические узлы и конец
|
|
|
|
|
|
track-а.
|
|
|
|
|
|
|
|
|
|
|
|
Для времени между двумя keys:
|
|
|
|
|
|
|
|
|
|
|
|
```text
|
|
|
|
|
|
alpha = (t - k0.time) / (k1.time - k0.time)
|
|
|
|
|
|
position = lerp(k0.position, k1.position, alpha)
|
|
|
|
|
|
rotation = shortest-path quaternion blend
|
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
|
|
Перед quaternion blend проверяется dot product. Если стороны находятся в
|
|
|
|
|
|
противоположных полусферах, знак второй стороны меняется, чтобы пройти по
|
|
|
|
|
|
короткому пути. При точном совпадении времени возвращается соответствующий key
|
|
|
|
|
|
без вычисления alpha.
|
|
|
|
|
|
|
|
|
|
|
|
Объект может переходить между двумя animation states. Тогда для каждого узла
|
|
|
|
|
|
сэмплируются позы A и B, затем position смешивается линейно, а quaternion --
|
|
|
|
|
|
через shortest-path blend. Если одна сторона невалидна, используется другая.
|
|
|
|
|
|
|
|
|
|
|
|
```c
|
|
|
|
|
|
Pose sample_node(Node n, float t);
|
|
|
|
|
|
Pose blend_pose(Pose a, Pose b, float weight);
|
|
|
|
|
|
Mat4 local = quaternion_matrix(pose.rotation);
|
|
|
|
|
|
local.set_translation(pose.position);
|
|
|
|
|
|
world[n] = world[parent(n)] * local;
|
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
|
|
Для parity особенно важны x87-compatible округление при выборе frame index и
|
|
|
|
|
|
порядок операций. Одинаковая формула на SSE может выбрать соседний кадр возле
|
|
|
|
|
|
границы.
|
|
|
|
|
|
|
|
|
|
|
|
Проверки animation data:
|
|
|
|
|
|
|
|
|
|
|
|
- размер type 8 кратен 24;
|
|
|
|
|
|
- размер type 19 кратен 2;
|
|
|
|
|
|
- каждый `fallback_key` меньше числа keys;
|
|
|
|
|
|
- блок карты узла полностью помещается в type 19;
|
|
|
|
|
|
- времена keys внутри track возрастают;
|
|
|
|
|
|
- parent links не образуют cycle;
|
|
|
|
|
|
- quaternion components читаются как signed 16-bit.
|
|
|
|
|
|
|
|
|
|
|
|
## WEAR и MAT0
|
|
|
|
|
|
|
|
|
|
|
|
MSH batch хранит только числовой `material_index`. WEAR переводит позиционный
|
|
|
|
|
|
slot в имя материала. MAT0 по этому имени описывает phases, parameters,
|
|
|
|
|
|
texture names и animation blocks. Такое разделение позволяет одной geometry
|
|
|
|
|
|
использовать разные appearances.
|
|
|
|
|
|
|
|
|
|
|
|
```text
|
|
|
|
|
|
Batch20.material_index
|
|
|
|
|
|
-> строка WEAR
|
|
|
|
|
|
-> имя MAT0
|
|
|
|
|
|
-> активная phase
|
|
|
|
|
|
-> textureName и render parameters
|
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
|
|
### WEAR
|
|
|
|
|
|
|
|
|
|
|
|
WEAR имеет type ID `0x52414557` и обычно хранится как `*.wea` рядом с моделью.
|
|
|
|
|
|
Формат текстовый:
|
|
|
|
|
|
|
|
|
|
|
|
```text
|
|
|
|
|
|
<wearCount>
|
|
|
|
|
|
<legacyId> <materialName>
|
|
|
|
|
|
... wearCount строк
|
|
|
|
|
|
|
|
|
|
|
|
[пустая строка]
|
|
|
|
|
|
[LIGHTMAPS
|
|
|
|
|
|
<lightmapCount>
|
|
|
|
|
|
<legacyId> <lightmapName>
|
|
|
|
|
|
... lightmapCount строк]
|
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
|
|
`legacyId` читается и сохраняется, но material выбирается по позиции строки и
|
|
|
|
|
|
имени. Пустая строка перед `LIGHTMAPS` является частью совместимого framing:
|
|
|
|
|
|
parser paths по-разному обрабатывают переход, и отсутствие разделителя ломает
|
|
|
|
|
|
совместимость. Material handle кодируется как `(table_index << 16) |
|
|
|
|
|
|
wear_index`; manager поддерживает ограниченное число wear tables.
|
|
|
|
|
|
|
|
|
|
|
|
Fallback material resolve строго разделён:
|
|
|
|
|
|
|
|
|
|
|
|
1. имя из WEAR;
|
|
|
|
|
|
2. `DEFAULT`;
|
|
|
|
|
|
3. entry 0;
|
|
|
|
|
|
4. для lightmap отсутствие означает slot `-1`, а не замену обычной texture.
|
|
|
|
|
|
|
|
|
|
|
|
Пустое имя texture внутри phase означает намеренно untextured surface.
|
|
|
|
|
|
Lightmap ищется в отдельном cache и не подменяется diffuse texture.
|
|
|
|
|
|
|
|
|
|
|
|
### MAT0
|
|
|
|
|
|
|
|
|
|
|
|
MAT0 имеет type ID `0x3054414D` и обычно находится в `Material.lib`. `attr1`
|
|
|
|
|
|
содержит runtime flags, `attr2` -- версию payload. Versioned metadata читается
|
|
|
|
|
|
cursor-ом: старые версии получают runtime defaults, но reader не пытается
|
|
|
|
|
|
насильно читать поля новой версии.
|
|
|
|
|
|
|
|
|
|
|
|
```c
|
|
|
|
|
|
#pragma pack(push, 1)
|
|
|
|
|
|
struct Mat0PrefixV4Plus {
|
|
|
|
|
|
uint16_t phase_count; // +0x00
|
|
|
|
|
|
uint16_t animation_block_count; // +0x02, меньше 20
|
|
|
|
|
|
uint8_t metadata_a; // +0x04, attr2 >= 2
|
|
|
|
|
|
uint8_t metadata_b; // +0x05, attr2 >= 2
|
|
|
|
|
|
uint32_t metadata_c_raw; // +0x06, attr2 >= 3
|
|
|
|
|
|
uint32_t metadata_d_raw; // +0x0A, attr2 >= 4
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
struct Phase34 {
|
|
|
|
|
|
uint8_t parameters[18];
|
|
|
|
|
|
char texture_name[16];
|
|
|
|
|
|
};
|
|
|
|
|
|
#pragma pack(pop)
|
|
|
|
|
|
static_assert(sizeof(Phase34) == 34);
|
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
|
|
Если `attr2 < 2`, metadata A/B получают default `255`; при `attr2 < 3`
|
|
|
|
|
|
значение C соответствует `1.0f`; при `attr2 < 4` D равно 0. C/D сохраняются
|
|
|
|
|
|
как raw 32-bit values до полного подтверждения интерпретации. Phase parameters
|
|
|
|
|
|
сохраняются как 18 raw bytes даже там, где часть bytes уже имеет понятный
|
|
|
|
|
|
смысл.
|
|
|
|
|
|
|
|
|
|
|
|
Каждая phase разворачивается в runtime-запись примерно 76 байт: коэффициенты
|
|
|
|
|
|
цвета, освещения и прозрачности, texture slot и служебные поля. Material time
|
|
|
|
|
|
выбирает одну или две phases; только часть полей интерполируется, остальные
|
|
|
|
|
|
копируются из активной записи.
|
|
|
|
|
|
|
|
|
|
|
|
Animation block MAT0 имеет плотный framing без 4-byte tail alignment:
|
|
|
|
|
|
|
|
|
|
|
|
```text
|
|
|
|
|
|
u32 header_raw
|
|
|
|
|
|
u16 key_count
|
|
|
|
|
|
repeat key_count:
|
|
|
|
|
|
u16 k0
|
|
|
|
|
|
u16 k1
|
|
|
|
|
|
u16 k2
|
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
|
|
Младшие три бита `header_raw` задают числовой mode, остальные образуют mask
|
|
|
|
|
|
interpolation. Наблюдаются modes 0, 1, 2 и 3, связанные с семействами loop,
|
|
|
|
|
|
ping-pong, one-shot/clamp и random-offset, но точные boundary cases остаются
|
|
|
|
|
|
предметом runtime parity. Поле `k2` сохраняется всегда.
|
|
|
|
|
|
|
|
|
|
|
|
Проверки MAT0:
|
|
|
|
|
|
|
|
|
|
|
|
- `animation_block_count < 20`;
|
|
|
|
|
|
- все versioned metadata помещаются в payload;
|
|
|
|
|
|
- секция phases имеет ровно `phase_count * 34` байта;
|
|
|
|
|
|
- `texture_name` ограничено 16 байтами;
|
|
|
|
|
|
- каждый animation block и его keys помещаются в payload;
|
|
|
|
|
|
- parser заканчивает чтение на точном конце записи.
|
|
|
|
|
|
|
|
|
|
|
|
Material manager кэширует разобранный MAT0 и texture handles. Current phase
|
|
|
|
|
|
лучше вычислять на экземпляр материала, если random offset или локальное время
|
|
|
|
|
|
различаются между объектами; immutable phase data остаются общими.
|
|
|
|
|
|
|
|
|
|
|
|
## Texm: текстуры, mip-уровни и атласы
|
|
|
|
|
|
|
|
|
|
|
|
`Texm` -- основной формат изображений. Он хранится в `Textures.lib`,
|
|
|
|
|
|
`LightMap.lib` и других NRes-архивах. Payload содержит header, необязательную
|
|
|
|
|
|
palette, mip chain и иногда `Page` chunk для atlas rectangles.
|
|
|
|
|
|
|
|
|
|
|
|
```c
|
|
|
|
|
|
struct TexmHeader32 {
|
|
|
|
|
|
uint32_t magic; // 'Texm'
|
|
|
|
|
|
uint32_t width;
|
|
|
|
|
|
uint32_t height;
|
|
|
|
|
|
uint32_t mip_count;
|
|
|
|
|
|
uint32_t flags4;
|
|
|
|
|
|
uint32_t flags5;
|
|
|
|
|
|
uint32_t unknown6;
|
|
|
|
|
|
uint32_t format;
|
|
|
|
|
|
};
|
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
|
|
Подтверждённые formats:
|
|
|
|
|
|
|
|
|
|
|
|
```text
|
|
|
|
|
|
0 Indexed8 + palette 256 x 4 байта
|
|
|
|
|
|
565 R5 G6 B5
|
|
|
|
|
|
556 R5 G5 B6
|
|
|
|
|
|
4444 A4 R4 G4 B4
|
|
|
|
|
|
88 L8 A8
|
|
|
|
|
|
888 RGB8 в четырёхбайтовом element
|
|
|
|
|
|
8888 A8 R8 G8 B8
|
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
|
|
Formats 556 и 88 являются loader-confirmed, но не corpus-verified для
|
|
|
|
|
|
доступных игровых payload. CPU decoder расширяет короткие каналы до 8 bit через
|
|
|
|
|
|
повторение значимых bit, а не простым shift. Для 888 служебный четвёртый byte
|
|
|
|
|
|
сохраняется при roundtrip.
|
|
|
|
|
|
|
|
|
|
|
|
Layout:
|
|
|
|
|
|
|
|
|
|
|
|
```text
|
|
|
|
|
|
TexmHeader32
|
|
|
|
|
|
[palette 1024 байта, только для format 0]
|
|
|
|
|
|
level 0 pixels
|
|
|
|
|
|
level 1 pixels
|
|
|
|
|
|
...
|
|
|
|
|
|
level mip_count-1 pixels
|
|
|
|
|
|
[optional Page chunk]
|
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
|
|
Размер уровня `i` вычисляется из `max(1, width >> i)` и
|
|
|
|
|
|
`max(1, height >> i)`. Bytes per pixel: 1 для indexed; 2 для 565, 556, 4444 и
|
|
|
|
|
|
88; 4 для 888 и 8888. Parser суммирует размеры с проверкой overflow до чтения.
|
|
|
|
|
|
|
|
|
|
|
|
`Page` chunk:
|
|
|
|
|
|
|
|
|
|
|
|
```c
|
|
|
|
|
|
struct PageHeader8 {
|
|
|
|
|
|
uint32_t magic; // 'Page'
|
|
|
|
|
|
uint32_t rect_count;
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
struct PageRect8 {
|
|
|
|
|
|
int16_t x;
|
|
|
|
|
|
int16_t width;
|
|
|
|
|
|
int16_t y;
|
|
|
|
|
|
int16_t height;
|
|
|
|
|
|
};
|
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
|
|
Chunk обязан иметь размер `8 + rect_count * 8`; произвольный tail не
|
|
|
|
|
|
допускается. Rectangles задаются в pixel space базового mip. Если loader
|
|
|
|
|
|
пропускает верхние mip-уровни, rectangles масштабируются вместе с новым base
|
|
|
|
|
|
level.
|
|
|
|
|
|
|
|
|
|
|
|
Mip-skip является поведением loader-а, а не offline-изменением файла. После
|
|
|
|
|
|
skip меняются runtime width, height, mip count и pointer на первый загружаемый
|
|
|
|
|
|
уровень. Современный renderer должен повторить выбор base level или
|
|
|
|
|
|
эквивалентно эмулировать его upload policy; использование полной texture при
|
|
|
|
|
|
тех же UV меняет резкость и atlas coordinates.
|
|
|
|
|
|
|
|
|
|
|
|
Indexed texture требует связанную palette. Часть palettes выбирается по suffix
|
|
|
|
|
|
имени: буква `A..Z` и вариант пустой или `0..9`, всего 286 возможных slots.
|
|
|
|
|
|
Невалидный suffix диагностируется явно.
|
|
|
|
|
|
|
|
|
|
|
|
Обычные textures и lightmaps находятся в разных managers. Обычный cache
|
|
|
|
|
|
отслеживает refcount и время неиспользования, а eviction выполняется
|
|
|
|
|
|
отложенно. Lightmap lifetime связан с world/mission и не должен попадать под
|
|
|
|
|
|
ту же политику удаления.
|
|
|
|
|
|
|
|
|
|
|
|
Строгий Texm parser проверяет положительные dimensions, положительный
|
|
|
|
|
|
`mip_count`, известный format, точный размер palette/mip chain, корректный
|
|
|
|
|
|
`Page` и отсутствие лишних bytes. `flags4`, `flags5` и `unknown6` сохраняются
|
|
|
|
|
|
1:1; участие `flags5` в mip-skip подтверждено, но полная семантика всех bits не
|
|
|
|
|
|
закрыта.
|
|
|
|
|
|
|
|
|
|
|
|
## Свет, тени, атмосфера и сортировка
|
|
|
|
|
|
|
|
|
|
|
|
Свет является отдельной world-подсистемой. Terrain layer создаёт
|
|
|
|
|
|
`LightManager`, `Shader` и primitive managers. Это не один глобальный
|
|
|
|
|
|
коэффициент яркости: world управляет point lights, lightmaps, shadows,
|
|
|
|
|
|
atmospheric objects и sort phases. Материал сообщает свойства поверхности, а
|
|
|
|
|
|
CShade превращает их в states renderer-а.
|
|
|
|
|
|
|
|
|
|
|
|
Подтверждённые точки: `CreateLightManager`, `CreateShader`,
|
|
|
|
|
|
`CreateAtmosphere`, `CreatePrimitives`, `CreatePrimitives2`,
|
|
|
|
|
|
`CShade::StartMeshRender`, `CShade::EndMeshRender` и
|
|
|
|
|
|
`CShade::ConfigureTextureAndAlphaBlendModes`.
|
|
|
|
|
|
|
|
|
|
|
|
CShade получает active MAT0 phase, capability profile устройства и pass
|
|
|
|
|
|
context. Он выбирает texture mode, alpha blending, depth/cull behavior и способ
|
|
|
|
|
|
освещения. Наличие fallback вроде `TEXTUREMODE_MODULATE not supported`
|
|
|
|
|
|
означает, что material нельзя напрямую преобразовать в современный PBR.
|
|
|
|
|
|
Сначала строится legacy state, затем он сопоставляется shader permutation.
|
|
|
|
|
|
|
|
|
|
|
|
CLightManager выдаёт numeric IDs источникам и проверяет допустимое количество.
|
|
|
|
|
|
Ветка `EmulatePointLights()` позволяет воспроизводить point lights даже при
|
|
|
|
|
|
ограничениях hardware lighting. Неизвестный type light должен давать отдельную
|
|
|
|
|
|
ошибку.
|
|
|
|
|
|
|
|
|
|
|
|
Lightmap не является обычной diffuse texture. WEAR содержит отдельный блок
|
|
|
|
|
|
`LIGHTMAPS`, manager открывает `LightMap.lib`, а shade path подаёт lightmap
|
|
|
|
|
|
отдельным slot или texture stage. Замена lightmap предварительным умножением в
|
|
|
|
|
|
diffuse texture ломает LOD, atlas coordinates и динамическую модуляцию.
|
|
|
|
|
|
|
|
|
|
|
|
Тени проходят отдельным render pass. Terrain содержит пути для теней зданий и
|
|
|
|
|
|
роботов, ограничения максимального числа, detail level и smoothing. Доказаны
|
|
|
|
|
|
shadow manager/pass, настройки detail/smoothing/count и зависимость от
|
|
|
|
|
|
Terrain/CShade; полная формула projection geometry для каждого caster требует
|
|
|
|
|
|
dynamic trace. Unknown settings из `shade.cfg` читаются и сохраняются по
|
|
|
|
|
|
именам, а не заменяются произвольными modern defaults.
|
|
|
|
|
|
|
|
|
|
|
|
Atmosphere manager создаёт world objects для фоновых и погодных явлений.
|
|
|
|
|
|
Отдельно подтверждены lightning, sun render, flare, `env_lightning`, rain
|
|
|
|
|
|
background sound и обязательные ссылки на lightning effect. Эти объекты
|
|
|
|
|
|
обновляются по игровому времени, но часть параметров зависит от camera: flare
|
|
|
|
|
|
требует screen position и occlusion test, rain -- области рядом с observer,
|
|
|
|
|
|
sound -- listener. Их нельзя один раз запечь в terrain.
|
|
|
|
|
|
|
|
|
|
|
|
RNG для lightning, atmosphere phases и FX должен иметь стабильный порядок.
|
|
|
|
|
|
Даже правильный средний интервал не даёт повторяемый кадр, если random values
|
|
|
|
|
|
запрашиваются в другой последовательности.
|
|
|
|
|
|
|
|
|
|
|
|
Согласованная модель sort phases:
|
|
|
|
|
|
|
|
|
|
|
|
```text
|
|
|
|
|
|
opaque terrain and models
|
|
|
|
|
|
-> lightmapped/state-grouped passes
|
|
|
|
|
|
-> shadows and projected primitives
|
|
|
|
|
|
-> alpha-tested surfaces
|
|
|
|
|
|
-> transparent objects/effects back-to-front
|
|
|
|
|
|
-> atmosphere, flares and overlays
|
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
|
|
Точный взаимный порядок отдельных FX, shadow и atmosphere subpasses требует
|
|
|
|
|
|
capture. Новый renderer должен хранить явный `RenderPhase` и стабильный
|
|
|
|
|
|
secondary sort key, а не сортировать всё только по material ID.
|
|
|
|
|
|
|
|
|
|
|
|
## FXID: система эффектов
|
|
|
|
|
|
|
|
|
|
|
|
FXID -- не готовая картинка, а описание небольшого runtime command stream.
|
|
|
|
|
|
Header задаёт lifetime, time mode, random shifts и transform. Затем идут
|
|
|
|
|
|
команды разных types. При создании manager превращает disk-команды в runtime
|
|
|
|
|
|
objects; во время кадра они обновляются и выпускают sounds, particles,
|
|
|
|
|
|
materials или projected primitives.
|
|
|
|
|
|
|
|
|
|
|
|
Type ID равен `0x44495846`. Header занимает 60 байт:
|
|
|
|
|
|
|
|
|
|
|
|
```c
|
|
|
|
|
|
struct FxHeader60 {
|
|
|
|
|
|
uint32_t command_count;
|
|
|
|
|
|
uint32_t time_mode;
|
|
|
|
|
|
float duration_seconds;
|
|
|
|
|
|
float phase_jitter;
|
|
|
|
|
|
uint32_t flags;
|
|
|
|
|
|
uint32_t settings_id;
|
|
|
|
|
|
float random_shift[3];
|
|
|
|
|
|
float pivot[3];
|
|
|
|
|
|
float scale[3];
|
|
|
|
|
|
};
|
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
|
|
Поток команд начинается строго с offset `0x3C`. `duration_seconds`
|
|
|
|
|
|
преобразуется runtime-ом во внутреннюю шкалу времени. `phase_jitter` и
|
|
|
|
|
|
`random_shift` используются только при соответствующих flags. Pivot задаёт
|
|
|
|
|
|
локальную точку опоры, scale -- базовый масштаб экземпляра. Unknown flags и
|
|
|
|
|
|
settings ID сохраняются.
|
|
|
|
|
|
|
|
|
|
|
|
Каждая команда начинается с `uint32_t command_word`:
|
|
|
|
|
|
|
|
|
|
|
|
```text
|
|
|
|
|
|
opcode = command_word & 0xFF
|
|
|
|
|
|
enabled = (command_word >> 8) & 1
|
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
|
|
Bits 9-31 являются частью данных и сохраняются. Между командами нет
|
|
|
|
|
|
выравнивания. Размер команды, включая word:
|
|
|
|
|
|
|
|
|
|
|
|
```text
|
|
|
|
|
|
opcode 1 224 байта
|
|
|
|
|
|
opcode 2 148 байт
|
|
|
|
|
|
opcode 3 200 байт
|
|
|
|
|
|
opcode 4 204 байта
|
|
|
|
|
|
opcode 5 112 байт
|
|
|
|
|
|
opcode 6 4 байта
|
|
|
|
|
|
opcode 7 208 байт
|
|
|
|
|
|
opcode 8 248 байт
|
|
|
|
|
|
opcode 9 208 байт
|
|
|
|
|
|
opcode 10 208 байт
|
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
|
|
Parser использует opcode только для выбора фиксированного размера. Неизвестный
|
|
|
|
|
|
opcode отклоняется: попытка угадать длину потеряет синхронизацию всего stream.
|
|
|
|
|
|
|
|
|
|
|
|
Opcodes 2, 3, 4, 5, 7, 8, 9 и 10 содержат pair fixed strings:
|
|
|
|
|
|
|
|
|
|
|
|
```c
|
|
|
|
|
|
struct FxResourceRef64 {
|
|
|
|
|
|
char archive[32];
|
|
|
|
|
|
char name[32];
|
|
|
|
|
|
};
|
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
|
|
Имена сравниваются case-insensitive по ASCII, а tail после первого nul byte
|
|
|
|
|
|
сохраняется. Resolve выполняется при создании command object или лениво при
|
|
|
|
|
|
первом запуске, но ошибка должна включать имя эффекта, номер команды, archive
|
|
|
|
|
|
и resource name.
|
|
|
|
|
|
|
|
|
|
|
|
Базовый normalized age:
|
|
|
|
|
|
|
|
|
|
|
|
```text
|
|
|
|
|
|
tn = (now - start_time) / (end_time - start_time)
|
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
|
|
`time_mode` выбирает источник коэффициента: constant, forward/reverse age,
|
|
|
|
|
|
cyclic phase, external world state и варианты с ограничением относительно
|
|
|
|
|
|
предыдущего значения. Точные формулы редких modes являются parity-задачей.
|
|
|
|
|
|
Flags могут умножать alpha на lifetime, применять triangular remap, случайно
|
|
|
|
|
|
сдвигать phase/space, инвертировать active-state, фильтровать по времени суток
|
|
|
|
|
|
или включать manager gates.
|
|
|
|
|
|
|
|
|
|
|
|
Lifecycle:
|
|
|
|
|
|
|
|
|
|
|
|
```text
|
|
|
|
|
|
create instance
|
|
|
|
|
|
-> copy header and external transform
|
|
|
|
|
|
-> calculate end time and random offsets
|
|
|
|
|
|
-> create command objects in disk order
|
|
|
|
|
|
-> resolve required resources
|
|
|
|
|
|
-> Start
|
|
|
|
|
|
|
|
|
|
|
|
on each calculation/render frame
|
|
|
|
|
|
-> evaluate time coefficient and gates
|
|
|
|
|
|
-> update commands in stable order
|
|
|
|
|
|
-> emit active primitives or sounds
|
|
|
|
|
|
-> collect render batches
|
|
|
|
|
|
-> handle Stop / Restart / end-of-life
|
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
|
|
Update и emit разделяются. Simulation может продолжаться в кадре без render, а
|
|
|
|
|
|
emit не должен повторно менять игровое состояние. Для authoring безопасно
|
|
|
|
|
|
типизировать header и resource references, а body редких commands сохранять raw
|
|
|
|
|
|
до подтверждения field-level semantics.
|
|
|
|
|
|
|
|
|
|
|
|
## Полный кадр
|
|
|
|
|
|
|
|
|
|
|
|
Крупный вход в world render проходит через `World3D::stdRenderGame`. Доказан
|
|
|
|
|
|
следующий порядок boundary операций:
|
|
|
|
|
|
|
|
|
|
|
|
1. передать camera в Terrain через `stdSetCurrentCamera2` и сохранить её как
|
|
|
|
|
|
текущую;
|
|
|
|
|
|
2. получить camera/view/viewport interfaces через virtual queries;
|
|
|
|
|
|
3. обновить положение и ориентацию 3D sound listener;
|
|
|
|
|
|
4. настроить renderer viewport и matrices;
|
|
|
|
|
|
5. вызвать два renderer boundary slots перед traversal;
|
|
|
|
|
|
6. установить глобальный флаг `in_render`;
|
|
|
|
|
|
7. вызвать главный virtual метод camera/world traversal;
|
|
|
|
|
|
8. выполнить дополнительную post queue при включённом режиме;
|
|
|
|
|
|
9. завершить world/shade pass;
|
|
|
|
|
|
10. вызвать renderer completion slot;
|
|
|
|
|
|
11. снять `in_render`, восстановить viewport и разослать end-of-render.
|
|
|
|
|
|
|
|
|
|
|
|
Семантические имена нескольких slots перед и после traversal не подтверждены,
|
|
|
|
|
|
поэтому в compatibility code их лучше временно называть
|
|
|
|
|
|
`frame_boundary_0`, `frame_boundary_1`, `frame_boundary_2`.
|
|
|
|
|
|
|
|
|
|
|
|
Обход видимого мира:
|
|
|
|
|
|
|
|
|
|
|
|
```text
|
|
|
|
|
|
проверить active/visible state
|
|
|
|
|
|
-> выбрать LOD по расстоянию и настройкам
|
|
|
|
|
|
-> получить node matrices из animation state
|
|
|
|
|
|
-> выбрать slot для каждого node/group
|
|
|
|
|
|
-> преобразовать bounds в world space
|
|
|
|
|
|
-> выполнить culling
|
|
|
|
|
|
-> добавить batches в подходящую render queue
|
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
|
|
Material/texture resolve желательно выполнять после visibility и slot
|
|
|
|
|
|
selection, чтобы невидимые объекты не меняли порядок обращений к caches и не
|
|
|
|
|
|
создавали лишние side effects. Невидимость объекта и отсутствие slot являются
|
|
|
|
|
|
разными причинами пропуска и диагностируются отдельно.
|
|
|
|
|
|
|
|
|
|
|
|
Подготовленный draw item содержит:
|
|
|
|
|
|
|
|
|
|
|
|
```text
|
|
|
|
|
|
node world matrix
|
|
|
|
|
|
batch flags and index range
|
|
|
|
|
|
WEAR material handle
|
|
|
|
|
|
MAT0 active phase and coefficients
|
|
|
|
|
|
texture handle
|
|
|
|
|
|
optional lightmap handle
|
|
|
|
|
|
render phase and sorting key
|
|
|
|
|
|
legacy pipeline state
|
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
|
|
Draw item должен ссылаться на immutable данные кадра. Изменение phase или
|
|
|
|
|
|
texture cache посреди прохода не должно менять уже собранную очередь.
|
|
|
|
|
|
|
|
|
|
|
|
Согласованная декомпозиция внутренних render phases:
|
|
|
|
|
|
|
|
|
|
|
|
1. подготовка frame state, camera и viewport;
|
|
|
|
|
|
2. непрозрачный terrain;
|
|
|
|
|
|
3. непрозрачные object batches;
|
|
|
|
|
|
4. lightmap и дополнительные material passes;
|
|
|
|
|
|
5. projected primitives и тени;
|
|
|
|
|
|
6. alpha-tested geometry;
|
|
|
|
|
|
7. transparent objects и FX в сортировочных слоях;
|
|
|
|
|
|
8. atmosphere, sun, flare и weather;
|
|
|
|
|
|
9. renderer completion boundary;
|
|
|
|
|
|
10. end-of-render callbacks;
|
|
|
|
|
|
11. shell/UI и post-render state.
|
|
|
|
|
|
|
|
|
|
|
|
Точный взаимный порядок пунктов 4-8 и связь completion slot с физическим
|
|
|
|
|
|
DirectDraw flip/present требуют dynamic capture. Сортировка внутри каждой фазы
|
|
|
|
|
|
должна быть стабильной: для opaque первичен pipeline/material key, для
|
|
|
|
|
|
transparent -- distance layer и depth order, затем stable insertion ID.
|
|
|
|
|
|
|
|
|
|
|
|
Геометрический draw использует streams type 3/4/5, optional streams, index
|
|
|
|
|
|
buffer type 6, `base_vertex`, `index_start` и `index_count`. Матрица узла
|
|
|
|
|
|
устанавливается как world transform, затем CShade привязывает texture stages и
|
|
|
|
|
|
fixed-function state.
|
|
|
|
|
|
|
|
|
|
|
|
```c
|
|
|
|
|
|
set_world_matrix(item.node_world);
|
|
|
|
|
|
bind_vertex_streams(model.streams);
|
|
|
|
|
|
bind_index_buffer(model.indices);
|
|
|
|
|
|
apply_legacy_state(item.pipeline);
|
|
|
|
|
|
bind_texture(0, item.texture);
|
|
|
|
|
|
bind_texture(1, item.lightmap);
|
|
|
|
|
|
draw_indexed(item.batch.base_vertex,
|
|
|
|
|
|
item.batch.index_start,
|
|
|
|
|
|
item.batch.index_count);
|
|
|
|
|
|
```
|
|
|
|
|
|
|
2026-07-18 08:18:39 +04:00
|
|
|
|
## Текущий контракт pipeline key
|
|
|
|
|
|
|
|
|
|
|
|
`fparkan-render` теперь переносит вместе с каждым backend-neutral draw
|
|
|
|
|
|
`LegacyPipelineState` и детерминированный `PipelineKey`. Ключ не хешируется:
|
|
|
|
|
|
он явно упаковывает blend mode, depth mode, cull mode и флаг alpha-test, поэтому
|
|
|
|
|
|
одинаковые snapshots дают одинаковый capture и будущий Vulkan cache не зависит
|
|
|
|
|
|
от версии Rust или процесса. Alpha reference намеренно не входит в key: это
|
|
|
|
|
|
dynamic material constant, а не структурный вариант graphics pipeline.
|
|
|
|
|
|
|
2026-07-18 08:22:29 +04:00
|
|
|
|
Текущий static Vulkan viewer дедуплицирует состояния ranges при каждой сборке
|
|
|
|
|
|
swapchain resources, создаёт один `vk::Pipeline` на уникальный `PipelineKey` и
|
|
|
|
|
|
выбирает его непосредственно перед соответствующим `vkCmdDrawIndexed`.
|
|
|
|
|
|
Кэш корректно уничтожается при recreate/teardown; setup failure откатывает уже
|
|
|
|
|
|
созданные variants. Baseline оригинального MSH подтверждён с состоянием
|
|
|
|
|
|
`opaque + depth disabled + cull disabled + alpha-test disabled`.
|
|
|
|
|
|
|
2026-07-18 08:31:19 +04:00
|
|
|
|
`SourceAlpha`, front/back culling и depth modes имеют Vulkan mapping в этом
|
|
|
|
|
|
кэше. При создании swapchain resources renderer выбирает capability-approved
|
|
|
|
|
|
depth/stencil format, выделяет device-local attachment, очищает его в render
|
|
|
|
|
|
pass и прикрепляет к каждому framebuffer. `TestWrite` включает depth test и
|
2026-07-18 08:40:40 +04:00
|
|
|
|
write; `TestReadOnly` включает test без write. Alpha test выполняется во
|
|
|
|
|
|
fragment shader: перед каждым draw renderer передаёт через push constant
|
|
|
|
|
|
`alpha_test_reference / 255`, а shader отбрасывает fragment с меньшей alpha.
|
|
|
|
|
|
При выключенном `alpha_test` cutoff принудительно равен нулю; reference остаётся
|
|
|
|
|
|
dynamic material data и не входит в `PipelineKey`. Также не установлен источник
|
|
|
|
|
|
значений для Batch20/MAT0: их
|
2026-07-18 08:22:29 +04:00
|
|
|
|
поля нельзя объявлять blend/depth/cull mapping без dynamic capture или
|
|
|
|
|
|
дополнительного дизассемблирования. Это частично реализованная compatibility
|
|
|
|
|
|
boundary, а не заявление о готовой parity fixed-function state.
|
2026-07-18 08:18:39 +04:00
|
|
|
|
|
2026-07-18 08:59:28 +04:00
|
|
|
|
### Capability-gated swapchain source for future pixel capture
|
|
|
|
|
|
|
|
|
|
|
|
Pixel parity требует чтения результата GPU, но swapchain image разрешает это
|
|
|
|
|
|
только если surface сообщает `VK_IMAGE_USAGE_TRANSFER_SRC_BIT`. Поэтому
|
|
|
|
|
|
swapchain policy всегда запрашивает обязательный `COLOR_ATTACHMENT`, а
|
|
|
|
|
|
`TRANSFER_SRC` добавляет лишь при фактической поддержке surface; выбранные raw
|
|
|
|
|
|
usage bits попадают в deterministic plan и native smoke report. Это не создаёт
|
|
|
|
|
|
readback buffer, не копирует pixels и не закрывает pixel-capture acceptance.
|
|
|
|
|
|
|
|
|
|
|
|
На Windows GOG native smoke 2026-07-18 (`MTCHECK.MSH` из `system.rlb` и
|
|
|
|
|
|
`DEFAULT.0` из `Textures.lib`) AMD Radeon Pro WX 3200 Series подтвердил
|
|
|
|
|
|
`image_usage=17` (`COLOR_ATTACHMENT | TRANSFER_SRC`): 300 frames, 3 resize
|
|
|
|
|
|
events, 2 swapchain recreations, Vulkan validation warnings/errors `0/0`.
|
|
|
|
|
|
Следующий шаг должен выполнить явный image-to-buffer copy и сравнить полученные
|
|
|
|
|
|
bytes с зафиксированным reference capture.
|
|
|
|
|
|
|
2026-07-18 09:05:20 +04:00
|
|
|
|
### First synchronized Vulkan pixel-readback artifact
|
|
|
|
|
|
|
|
|
|
|
|
Stage 3 static viewer теперь выполняет фактический readback для surface с
|
|
|
|
|
|
`TRANSFER_SRC`: на каждый swapchain image создаётся host-visible coherent
|
|
|
|
|
|
`TRANSFER_DST` buffer. После render pass command buffer переводит image из
|
|
|
|
|
|
`PRESENT_SRC_KHR` в `TRANSFER_SRC_OPTIMAL`, выполняет `vkCmdCopyImageToBuffer`
|
|
|
|
|
|
и возвращает image в `PRESENT_SRC_KHR`. CPU отображает memory только после
|
|
|
|
|
|
`vkDeviceWaitIdle` during shutdown; это исключает чтение GPU work in flight.
|
|
|
|
|
|
Smoke JSON фиксирует число записанных copy-команд, final byte count и FNV-1a-64
|
|
|
|
|
|
hash всех current-swapchain readback buffers, не сохраняя игровые pixels в
|
|
|
|
|
|
репозитории.
|
|
|
|
|
|
|
|
|
|
|
|
На GOG `MTCHECK.MSH`/`DEFAULT.0` AMD Radeon Pro WX 3200 Series выполнил 300
|
|
|
|
|
|
copy-команд, final artifact 4,147,200 bytes и hash `2184179010340020629` при
|
|
|
|
|
|
validation warnings/errors `0/0`. Повторный идентичный запуск дал тот же
|
|
|
|
|
|
размер и hash. Это доказывает Vulkan copy/readback path только для нашего
|
|
|
|
|
|
static viewer. Он ещё не захватывает original DirectDraw frame, не задаёт
|
|
|
|
|
|
fixed original camera и не сравнивает два изображения, поэтому pixel-parity
|
|
|
|
|
|
acceptance остаётся blocked.
|
|
|
|
|
|
|
2026-07-18 09:11:43 +04:00
|
|
|
|
Smoke также сохраняет raw artifact рядом с JSON: `<report-stem>.readback-vkformat-<raw>.raw`.
|
2026-07-18 09:08:07 +04:00
|
|
|
|
Это concatenated current-swapchain images in Vulkan order, каждый с dimensions
|
|
|
|
|
|
из JSON и четырьмя bytes per pixel; format намеренно указан в имени файла,
|
2026-07-18 09:11:43 +04:00
|
|
|
|
потому что bytes не перекодируются; JSON содержит actual raw enum. GOG selected `50` and produced a 4,147,200-byte file
|
2026-07-18 09:08:07 +04:00
|
|
|
|
for two 960x540 images. Артефакт остаётся локальным output и не попадает в Git.
|
|
|
|
|
|
|
2026-07-18 09:13:52 +04:00
|
|
|
|
Smoke option `--expected-readback <path>` выполняет exact byte comparison после
|
|
|
|
|
|
readback и завершает run с первым differing byte или length mismatch. Fresh GOG
|
|
|
|
|
|
run с предыдущим format-50 artifact прошёл этот gate. Это regression contract
|
|
|
|
|
|
нашего static Vulkan viewer, не comparison с оригинальным renderer.
|
2026-07-18 09:15:23 +04:00
|
|
|
|
Comparator прежде сравнивает `vkformat` identity из имен обоих artifacts и
|
|
|
|
|
|
отклоняет mismatch до byte scan; raw blobs разных Vulkan formats нельзя
|
|
|
|
|
|
считать сопоставимыми без явной conversion contract.
|
2026-07-18 09:13:52 +04:00
|
|
|
|
|
2026-07-18 09:19:56 +04:00
|
|
|
|
`fparkan-game` render-snapshot bridge больше не берёт только первый prepared
|
|
|
|
|
|
visual mission object. Каждый `MissionAssets::visuals_for_object` entry
|
|
|
|
|
|
создаёт отдельный backend-neutral draw с own mesh/material IDs; object без
|
|
|
|
|
|
prepared visual сохраняет один compatibility fallback draw. Это сохраняет
|
|
|
|
|
|
multi-component prototype graph для будущего real Vulkan renderer, однако
|
|
|
|
|
|
текущий game app всё ещё использует planning backend и triangle ranges.
|
|
|
|
|
|
|
2026-07-18 09:21:58 +04:00
|
|
|
|
### Mission position/scale render bridge
|
|
|
|
|
|
|
|
|
|
|
|
`fparkan-game` now maps preserved TMA position and non-uniform scale into each
|
|
|
|
|
|
backend-neutral draw matrix instead of diagnostic index placement. Raw
|
|
|
|
|
|
orientation deliberately remains uninterpreted: the original Euler order and
|
|
|
|
|
|
matrix convention are not yet proven. This preserves authored values without
|
|
|
|
|
|
inventing transform math; the app still uses planning backend and triangle
|
|
|
|
|
|
ranges, so it is not a full renderer claim.
|
|
|
|
|
|
|
2026-07-18 09:24:45 +04:00
|
|
|
|
### Mission original-ID render provenance
|
|
|
|
|
|
|
|
|
|
|
|
Each backend-neutral draw now retains the `OriginalObjectId` preserved by the
|
|
|
|
|
|
TMA loader for its source mission object. This gives capture and diagnostics a
|
|
|
|
|
|
stable link from a draw back to its original object record, including every
|
|
|
|
|
|
visual component emitted for that object. The bridge deliberately leaves the
|
|
|
|
|
|
value absent when no mission draft exists; it does not infer an original ID
|
|
|
|
|
|
from a runtime slot or draw order.
|
|
|
|
|
|
|
2026-07-18 09:31:32 +04:00
|
|
|
|
### Opt-in mission static-Vulkan bridge
|
|
|
|
|
|
|
|
|
|
|
|
`fparkan-game --backend static-vulkan` is an explicit native-window experiment,
|
2026-07-18 09:45:57 +04:00
|
|
|
|
not the default planning path. It visits only the first mission root, selects
|
2026-07-18 10:53:03 +04:00
|
|
|
|
every prepared MSH component of that root, merges their static XY clip-space
|
2026-07-18 10:06:34 +04:00
|
|
|
|
geometry, and renders a requested number of frames through
|
2026-07-18 09:31:32 +04:00
|
|
|
|
`VulkanSmokeRenderer`; teardown rejects validation warnings/errors and reports
|
2026-07-18 09:52:20 +04:00
|
|
|
|
swapchain/readback telemetry. For each used `Batch20.material_index`, it resolves
|
|
|
|
|
|
the positional prepared WEAR material and uploads mip 0 of that material's first
|
2026-07-18 10:06:34 +04:00
|
|
|
|
MAT0 diffuse texture request. Because `material_index` is local to an MSH/WEAR
|
|
|
|
|
|
component, the merged preview assigns a unique preview-local selector only after
|
|
|
|
|
|
this source resolution; it does not falsely treat equal local indexes as equal
|
|
|
|
|
|
materials.
|
2026-07-18 09:31:32 +04:00
|
|
|
|
|
|
|
|
|
|
This is a narrow bootstrap from mission assets to a live Vulkan renderer. It
|
2026-07-18 09:52:20 +04:00
|
|
|
|
does not render every placed object, apply TMA transforms or orientation, select
|
|
|
|
|
|
later MAT0 phases or animation, bind lightmaps, or establish a game camera. Fresh GOG
|
2026-07-18 09:45:57 +04:00
|
|
|
|
`MISSIONS/Autodemo.00/data.tma` evidence now proves the narrow GPU bridge: one
|
2026-07-18 10:06:34 +04:00
|
|
|
|
presented frame completed in 39.6 seconds with a native 1280×720 two-image
|
|
|
|
|
|
swapchain, 14 merged mesh components and 14 selector-keyed original diffuse
|
|
|
|
|
|
descriptors, 7,372,800-byte synchronized readback (FNV-1a
|
|
|
|
|
|
`16595193636416981301`) and validation
|
2026-07-18 09:52:20 +04:00
|
|
|
|
warnings/errors `0/0`. This is not a
|
2026-07-18 09:45:57 +04:00
|
|
|
|
full-scene or original-renderer pixel-parity claim.
|
|
|
|
|
|
|
2026-07-18 09:59:27 +04:00
|
|
|
|
The same bounded command was then run against the licensed installed Part 1
|
|
|
|
|
|
(`C:\\Program Files (x86)\\Nikita\\IS`) and Part 2
|
|
|
|
|
|
(`C:\\Program Files (x86)\\Nikita\\IS2`) corpora supplied for testing. Both
|
2026-07-18 10:06:34 +04:00
|
|
|
|
`MISSIONS/Autodemo.00/data.tma` runs completed with 14 mesh components and 14
|
|
|
|
|
|
original diffuse descriptors. Part 1 took 28.2 seconds and matched the GOG
|
|
|
|
|
|
readback hash `16595193636416981301`; Part 2 took 93.6 seconds and remained
|
|
|
|
|
|
validation-clean but produced distinct hash `18268338333658342130`. Part 2's
|
|
|
|
|
|
synchronous checkpoint was `Graph` while it was still loading, so the longer
|
|
|
|
|
|
startup is not attributed to Vulkan. This is only a cross-corpus confirmation
|
|
|
|
|
|
of the first-root static-preview bridge, not a claim that the two games' full
|
|
|
|
|
|
mission renderers are compatible.
|
2026-07-18 09:59:27 +04:00
|
|
|
|
|
2026-07-18 10:36:29 +04:00
|
|
|
|
The preview now has an explicit root-prefix contract. `--preview-roots N`
|
|
|
|
|
|
prepares the first non-zero `N` TMA roots for the opt-in static Vulkan path;
|
|
|
|
|
|
the default remains one root. Normal mission loading remains full and
|
|
|
|
|
|
transactional, so this is not a hidden relaxation of gameplay validation. The
|
|
|
|
|
|
previous all-root probe timed out at `Graph`; the prefix makes broader scene
|
|
|
|
|
|
work measurable without pretending that a bounded preview is the full game.
|
2026-07-18 09:36:29 +04:00
|
|
|
|
|
2026-07-18 10:21:43 +04:00
|
|
|
|
### Shared terrain/root diagnostic frame
|
|
|
|
|
|
|
|
|
|
|
|
The bounded native preview now retains the already validated `Land.msh` inside
|
|
|
|
|
|
`TerrainWorld`; this lets the application consume source geometry through the
|
|
|
|
|
|
runtime boundary instead of decoding archive formats a second time. It merges
|
|
|
|
|
|
one terrain component with the first TMA root's 14 reachable MSH components in
|
2026-07-18 10:53:03 +04:00
|
|
|
|
one explicit top-down XY frame. The frame is computed from terrain positions
|
2026-07-18 10:21:43 +04:00
|
|
|
|
and the root models after applying only the decoded TMA `position` and `scale`.
|
|
|
|
|
|
Raw TMA orientation is deliberately excluded: its convention is not proven.
|
|
|
|
|
|
|
|
|
|
|
|
Terrain is assigned an explicit 1x1 white diagnostic texture, rather than a
|
|
|
|
|
|
guessed terrain material. `Land.msh` slot selection, `material_tag`, masks,
|
|
|
|
|
|
auxiliary streams, original terrain phases and camera remain unresolved. Thus
|
|
|
|
|
|
this is an integrated geometry/provenance checkpoint, not a claim of original
|
|
|
|
|
|
terrain shading or scene camera reconstruction.
|
|
|
|
|
|
|
|
|
|
|
|
Fresh canonical GOG `MISSIONS/Autodemo.00/data.tma` evidence: one native
|
|
|
|
|
|
1280x720 Vulkan frame completed in 40.6 seconds with 14 root MSH components,
|
|
|
|
|
|
one terrain component, 15 descriptors, a 7,372,800-byte readback (FNV-1a
|
|
|
|
|
|
`10739087367165646439`) and validation warnings/errors `0/0`. The distinct hash
|
|
|
|
|
|
from the earlier first-root-only preview is expected because the submitted
|
|
|
|
|
|
geometry and descriptor set changed; it is not an original-frame comparison.
|
|
|
|
|
|
|
2026-07-18 10:36:29 +04:00
|
|
|
|
### Multiple static-preview roots
|
|
|
|
|
|
|
|
|
|
|
|
The native bridge now combines every mesh-backed visual of each selected root,
|
|
|
|
|
|
using that root's preserved TMA `position` and `scale` in the shared diagnostic
|
2026-07-18 10:53:03 +04:00
|
|
|
|
XY frame. It reports the actual selected root count as `preview_roots`, rather
|
2026-07-18 10:36:29 +04:00
|
|
|
|
than implying that all mission objects are rendered. Raw orientation, original
|
|
|
|
|
|
camera/frustum and visibility remain unresolved.
|
|
|
|
|
|
|
|
|
|
|
|
Fresh canonical GOG `Autodemo.00` evidence with `--preview-roots 2` completed
|
|
|
|
|
|
in 59.7 seconds: 25 submitted MSH components, one terrain component and 26
|
|
|
|
|
|
descriptors on the native 1280x720 two-image swapchain, with validation
|
|
|
|
|
|
warnings/errors `0/0`. The readback FNV-1a remained
|
|
|
|
|
|
`10739087367165646439`, equal to the one-root terrain/root run. That equality
|
2026-07-18 10:53:03 +04:00
|
|
|
|
does **not** prove the second root is visible or equivalent: it was recorded as
|
|
|
|
|
|
an investigation target, not hidden as a parity result.
|
2026-07-18 10:36:29 +04:00
|
|
|
|
|
2026-07-18 10:41:39 +04:00
|
|
|
|
The reproducible `fparkan-cli mission inspect` probe closes two tempting but
|
|
|
|
|
|
incorrect explanations for that equality. GOG `Autodemo.00` root 0 is
|
|
|
|
|
|
`w_m_wlk2.dat` at `(418.10318, 717.433, 3.0409389)` while root 1 is
|
|
|
|
|
|
`w_s_wlk1.dat` at `(479.12396, 795.95337, 1.6228507)`; therefore the TMA data
|
|
|
|
|
|
is neither duplicate nor co-located. Separately, the Vulkan static submit loop
|
|
|
|
|
|
calls `cmd_draw_indexed` for every prepared draw range and its depth comparison
|
2026-07-18 10:53:03 +04:00
|
|
|
|
is `LESS_OR_EQUAL`. A later direct `Land.msh` bounds probe identified the actual
|
|
|
|
|
|
static-viewer defect: GOG `AutoMAP/Land.msh` spans X/Y
|
|
|
|
|
|
`0..1190.6976` but Z only `0..94.50981`; the same TMA objects use X/Y for their
|
|
|
|
|
|
world placement and a small Z height. The old XZ CPU frame discarded the second
|
|
|
|
|
|
horizontal TMA component and treated height as the screen axis. The renderer now
|
|
|
|
|
|
uses a shared XY CPU frame and retains Z as geometry height. This proves only
|
|
|
|
|
|
the source-axis convention needed by the diagnostic bridge; it does not recover
|
|
|
|
|
|
the original camera, orientation order, culling or projection matrix.
|
|
|
|
|
|
|
|
|
|
|
|
The new `fparkan-cli terrain inspect <Land.msh>` exposes these three-axis source
|
|
|
|
|
|
bounds without a renderer. The first XY GPU rechecks were intentionally bounded
|
|
|
|
|
|
to 120 seconds but remained at the existing `Graph` loading checkpoint and were
|
|
|
|
|
|
terminated without a native frame/report; therefore no new readback hash or GPU
|
|
|
|
|
|
acceptance result is claimed for the corrected projection yet.
|
2026-07-18 10:41:39 +04:00
|
|
|
|
|
2026-07-18 11:02:46 +04:00
|
|
|
|
The load trace now splits base root expansion (`Graph`) from MSH/WEAR/MAT0/TEXM
|
|
|
|
|
|
visual-dependency expansion (`GraphVisuals`) and enters `Assets` immediately
|
|
|
|
|
|
before actual asset preparation. A controlled 60-second GOG first-root run of
|
|
|
|
|
|
the XY build stopped at `GraphVisuals`; the process was terminated by the probe
|
|
|
|
|
|
and produced no frame. The startup bottleneck is therefore narrowed to visual
|
|
|
|
|
|
graph expansion, rather than the base prototype graph, asset decoding, window
|
|
|
|
|
|
creation or Vulkan submission. This is a diagnostic boundary, not a claim that
|
|
|
|
|
|
the visual expansion is semantically optional or may be skipped.
|
|
|
|
|
|
|
2026-07-18 11:06:38 +04:00
|
|
|
|
Visual expansion now caches the validation result of each unique diffuse TEXM
|
|
|
|
|
|
and each unique lightmap TEXM independently. It still emits an edge and request
|
|
|
|
|
|
count for every original material reference, and it replays the same cached
|
|
|
|
|
|
success or failure at every reference; only duplicate archive open/read/decode
|
|
|
|
|
|
work is removed. Under the same controlled 60-second GOG first-root probe, the
|
|
|
|
|
|
last checkpoint advanced to `Assets`. The process was deliberately terminated
|
|
|
|
|
|
before a frame, so this proves reduced visual-graph work rather than a Vulkan
|
|
|
|
|
|
acceptance result.
|
|
|
|
|
|
|
2026-07-18 11:10:47 +04:00
|
|
|
|
Asset preparation additionally caches validated MSH models and decoded WEAR
|
|
|
|
|
|
tables by their complete resource key. This preserves separate prepared visual
|
|
|
|
|
|
provenance while avoiding repeat format work for repeated components. The same
|
|
|
|
|
|
60-second first-root GOG probe remained at `Assets`, so that corpus does not
|
|
|
|
|
|
demonstrate an additional checkpoint advance from this cache; it must not be
|
|
|
|
|
|
reported as a measured startup improvement.
|
2026-07-18 11:24:31 +04:00
|
|
|
|
|
|
|
|
|
|
Visual-graph material resolution now also caches the complete success or failure
|
|
|
|
|
|
of each WEAR material request. Every material edge and failure remains in the
|
|
|
|
|
|
graph; the cache only avoids reopening and decoding an already resolved MAT0.
|
|
|
|
|
|
Under the controlled 60-second GOG first-root probe, the phase sequence advanced
|
|
|
|
|
|
from `GraphVisuals` to `AssetModelMeshes`, `AssetWearTables`, then
|
|
|
|
|
|
`AssetTextures` before termination. This is a bounded timing observation, not a
|
|
|
|
|
|
claim that a frame completed or that every run has identical timing.
|
2026-07-18 11:29:22 +04:00
|
|
|
|
|
|
|
|
|
|
After these visual-graph caches, the corrected XY static-Vulkan preview again
|
|
|
|
|
|
completed on canonical GOG `MISSIONS/Autodemo.00/data.tma` within a controlled
|
|
|
|
|
|
120-second run. It reported `Complete`, a native 1280x720 two-image swapchain,
|
|
|
|
|
|
one frame, 14 root MSH components, one terrain component, 15 material
|
|
|
|
|
|
descriptors, validation warnings/errors `0/0`, and a 7,372,800-byte readback
|
|
|
|
|
|
FNV-1a `6451493914305554398`. This is the first GPU acceptance evidence for
|
|
|
|
|
|
the corrected XY diagnostic projection; the hash is not compared with the
|
|
|
|
|
|
original renderer and does not establish original camera, shading or parity.
|
2026-07-18 11:33:11 +04:00
|
|
|
|
|
|
|
|
|
|
The identical bounded XY command also completed against the supplied licensed
|
|
|
|
|
|
Part 1 installation (`C:\\Program Files (x86)\\Nikita\\IS`). Its report had
|
|
|
|
|
|
the same 14 root MSH components, terrain component, 15 descriptors, native
|
|
|
|
|
|
1280x720 two-image swapchain, validation `0/0`, and FNV-1a
|
|
|
|
|
|
`6451493914305554398`. This confirms the current first-root diagnostic bridge
|
|
|
|
|
|
across GOG and Part 1 data; it does not claim full mission or Part 2 parity.
|
2026-07-18 11:37:26 +04:00
|
|
|
|
|
|
|
|
|
|
The corresponding Part 2 run (`C:\\Program Files (x86)\\Nikita\\IS2`) did
|
|
|
|
|
|
not reach asset preparation or a Vulkan frame within a controlled 180-second
|
|
|
|
|
|
window: its final persisted checkpoint was `GraphVisuals`, and the exact child
|
|
|
|
|
|
process was terminated. This gives no Part 2 GPU acceptance or pixel result and
|
|
|
|
|
|
does not attribute the delay to Vulkan; its visual-dependency corpus remains a
|
|
|
|
|
|
separate profiling and compatibility target.
|
2026-07-18 11:43:33 +04:00
|
|
|
|
|
|
|
|
|
|
`GraphVisuals` is now split observationally into `GraphVisualWears`,
|
|
|
|
|
|
`GraphVisualMaterials` and `GraphVisualTextures`; it preserves the existing
|
|
|
|
|
|
graph traversal, edge order and validation semantics. Repeating the controlled
|
2026-07-18 12:16:45 +04:00
|
|
|
|
180-second Part 2 probe once ended at `GraphVisualTextures`. That particular
|
|
|
|
|
|
execution had passed WEAR and MAT0 expansion before timeout, but a checkpoint
|
|
|
|
|
|
is only the last phase reached in one run: it is not proof of a single global
|
|
|
|
|
|
bottleneck or of Vulkan involvement.
|
|
|
|
|
|
|
|
|
|
|
|
Visual expansion now reports each dependency-class entrance, its first request
|
2026-07-18 12:31:30 +04:00
|
|
|
|
and every 64th later request; runtime progress persists MAT0/TEXM counts at
|
|
|
|
|
|
those boundaries. This is diagnostic-only and does not alter traversal, edges
|
|
|
|
|
|
or validation. A
|
2026-07-18 12:16:45 +04:00
|
|
|
|
later controlled 180-second Part 2 probe ended at `GraphVisualMaterials`,
|
|
|
|
|
|
before the first TEXM progress marker. The differing checkpoints rule out the
|
|
|
|
|
|
previous overly narrow claim that TEXM is the sole remaining target. Profiling
|
|
|
|
|
|
must compare WEAR, MAT0, TEXM, graph allocation and archive I/O quantitatively.
|
2026-07-18 11:49:55 +04:00
|
|
|
|
|
2026-07-18 12:24:35 +04:00
|
|
|
|
Visual expansion now snapshots each prototype's base-graph `Prototype→MSH`
|
|
|
|
|
|
anchor before it appends any WEAR/MAT0/TEXM nodes and edges. The subsequent
|
|
|
|
|
|
traversal reads this immutable vector rather than repeatedly linearly searching
|
|
|
|
|
|
the growing graph; graph content, node/edge IDs and provenance remain
|
|
|
|
|
|
unchanged. The same controlled 180-second Part 2 probe still ended at
|
|
|
|
|
|
`GraphVisualMaterials`, so this is a complexity/correctness improvement rather
|
|
|
|
|
|
than measured evidence of a startup checkpoint advance.
|
|
|
|
|
|
|
2026-07-18 12:31:30 +04:00
|
|
|
|
The MAT0 counter now records `GraphVisualMaterialRequests(N)` with the same
|
|
|
|
|
|
64-request throttle. A fresh controlled Part 2 probe ended at
|
|
|
|
|
|
`GraphVisualMaterialRequests(71)`: at least 71 MAT0 requests had begun before
|
|
|
|
|
|
termination. It does not preserve the simultaneous TEXM count, so it neither
|
|
|
|
|
|
identifies a slow individual material nor attributes time to MAT0 parsing; the
|
|
|
|
|
|
next profiler revision needs one cumulative snapshot of all classes.
|
|
|
|
|
|
|
2026-07-18 13:24:00 +04:00
|
|
|
|
Visual MAT0/TEXM request markers now also carry the graph node and edge counts
|
|
|
|
|
|
materialized immediately before that request resolves. A rebuilt bounded Part 2
|
|
|
|
|
|
probe recorded MAT0 request 3 at 52 nodes / 51 edges and request 4 at 55 nodes
|
|
|
|
|
|
/ 54 edges. These are observational snapshots for graph-growth profiling only:
|
|
|
|
|
|
they do not change traversal order, node or edge identities, provenance,
|
|
|
|
|
|
validation, cache semantics, or rendering.
|
|
|
|
|
|
|
2026-07-18 13:28:27 +04:00
|
|
|
|
The same throttled snapshots now retain the number of distinct validation-cache
|
|
|
|
|
|
keys for WEAR, MAT0, and TEXM before the request. In a fresh 50-second Part 2
|
|
|
|
|
|
probe, MAT0 request 3 recorded cache counts 2 / 3 / 3 and request 4 recorded
|
|
|
|
|
|
3 / 4 / 4, while the graph moved from 52 / 51 to 55 / 54 nodes / edges. The
|
|
|
|
|
|
counts distinguish cache growth from graph growth without adding per-request
|
|
|
|
|
|
file writes or asserting which parser, archive I/O operation, or allocation
|
|
|
|
|
|
caused the elapsed interval.
|
|
|
|
|
|
|
2026-07-18 12:46:00 +04:00
|
|
|
|
`fparkan-game --load-progress` now initializes its file with `Starting` and
|
|
|
|
|
|
appends each later mission-load event instead of replacing the prior line. A
|
|
|
|
|
|
bounded probe can therefore retain both MAT0 and TEXM request milestones even
|
|
|
|
|
|
when a later event is last. The behavior is diagnostic persistence only: it
|
|
|
|
|
|
does not alter graph traversal, resource validation, or rendering.
|
|
|
|
|
|
|
2026-07-18 13:06:44 +04:00
|
|
|
|
Each trace row now keeps the phase name first and appends a monotonic
|
|
|
|
|
|
`elapsed_ms=<N>` field from one process-local `Instant`; successful completion
|
|
|
|
|
|
is appended rather than overwriting the trace. A short rebuilt Part 2 probe
|
|
|
|
|
|
confirmed real timestamps from `Map` at 5 ms through `GraphVisualTextures` at
|
|
|
|
|
|
2,389 ms, then later MAT0 request milestones at 28,841 and 35,486 ms. The
|
|
|
|
|
|
probe was deliberately terminated and provides no frame/GPU result. These
|
|
|
|
|
|
timestamps measure whole elapsed intervals, including archive I/O, allocation,
|
|
|
|
|
|
validation, cache effects and progress-file writes; they are not per-parser
|
|
|
|
|
|
benchmarks.
|
|
|
|
|
|
|
2026-07-18 13:10:46 +04:00
|
|
|
|
One bounded Part 2 trace reached `GraphVisualTextureRequests(64)` at 134,793
|
|
|
|
|
|
ms but did not reach `Assets`: its MAT0 markers were 3/4 at 28,823/35,473 ms,
|
|
|
|
|
|
13/16 at 88,260/88,334 ms, 25 at 108,137 ms, 38 at 121,369 ms, and 47 at
|
|
|
|
|
|
128,160 ms. The process was stopped immediately after the 134.8-second marker.
|
|
|
|
|
|
This makes visual-graph expansion, rather than asset preparation or Vulkan,
|
|
|
|
|
|
the measured unfinished interval for this run. It still does not allocate that
|
|
|
|
|
|
time among MAT0 decode, graph allocation, archive I/O, OS cache state, or the
|
|
|
|
|
|
interleaved TEXM requests.
|
|
|
|
|
|
|
2026-07-18 13:17:06 +04:00
|
|
|
|
Visual expansion now also caches WEAR validation by the complete derived WEAR
|
|
|
|
|
|
archive/name key and replays both successes and failures. It still creates each
|
|
|
|
|
|
per-prototype WEAR edge, increments the same request counters, and reports the
|
|
|
|
|
|
same failure provenance. A unit test verifies an archive-qualified cached
|
|
|
|
|
|
failure is replayed without consulting the repository. In a matched bounded
|
|
|
|
|
|
Part 2 probe the first TEXM-64 marker occurred at 135,520 ms, versus 134,793
|
|
|
|
|
|
ms in the preceding sample, and neither run reached `Assets`; therefore this
|
|
|
|
|
|
corpus does not evidence a startup advance from WEAR caching. The path is kept
|
|
|
|
|
|
as a correctness-preserving duplicate-resolve optimization, not a causal
|
|
|
|
|
|
performance claim.
|
|
|
|
|
|
|
2026-07-18 13:03:08 +04:00
|
|
|
|
A rebuilt executable then ran a controlled 180-second Part 2 `Autodemo.00`
|
|
|
|
|
|
probe with the append-only trace. Before exact-child termination it recorded
|
|
|
|
|
|
`GraphVisualTextureRequests(64)`, `GraphVisualMaterialRequests(100)`, and every
|
|
|
|
|
|
asset-preparation checkpoint through `AssetTextures`. It produced no native
|
|
|
|
|
|
window, frame, readback, or Vulkan acceptance report because it was terminated
|
|
|
|
|
|
while preparation was still active. This is one warm-state timing sample, not
|
|
|
|
|
|
evidence that the trace change improved loading or that either MAT0/TEXM alone
|
|
|
|
|
|
caused the earlier timeout; it does show the prior last-event-only trace hid
|
|
|
|
|
|
concurrent request classes and later preparation progress.
|
|
|
|
|
|
|
2026-07-18 11:49:55 +04:00
|
|
|
|
Mission loading now raises the decoded-payload cache entry budget from 64 to
|
|
|
|
|
|
256 while retaining its 64 MiB byte budget. This avoids premature entry-count
|
|
|
|
|
|
eviction during resource-rich loads without making memory unbounded. The same
|
|
|
|
|
|
180-second Part 2 probe nevertheless remained at `GraphVisualTextures`; this
|
|
|
|
|
|
change is a safe cache-capacity improvement, not measured evidence of a Part 2
|
|
|
|
|
|
startup advance.
|
2026-07-18 11:10:47 +04:00
|
|
|
|
|
2026-07-18 11:20:13 +04:00
|
|
|
|
`Assets` now has four ordered diagnostic sub-checkpoints: `AssetModelMeshes`
|
|
|
|
|
|
(MSH), `AssetWearTables` (WEAR), `AssetMaterials` (MAT0) and `AssetTextures`
|
|
|
|
|
|
(diffuse TEXM and lightmaps). The callback is observational: it neither changes
|
|
|
|
|
|
the preparation order nor deduplicates requests. A fresh controlled 60-second
|
|
|
|
|
|
canonical GOG `Autodemo.00` first-root probe reached `AssetWearTables` but not
|
2026-07-18 11:15:43 +04:00
|
|
|
|
`AssetMaterials`; its created process was terminated and no native frame was
|
2026-07-18 11:20:13 +04:00
|
|
|
|
reported. At least one MSH therefore finished before the timeout, but this does
|
|
|
|
|
|
not attribute the remaining interval to WEAR decoding rather than later
|
|
|
|
|
|
MSH/WEAR iteration or archive I/O.
|
2026-07-18 11:15:43 +04:00
|
|
|
|
|
2026-07-18 10:57:21 +04:00
|
|
|
|
The same source-axis proof now applies to `TerrainWorld`: its `Land.msh` surface
|
|
|
|
|
|
height query and `Land.map` areal/grid lookup use XY ground coordinates, return
|
|
|
|
|
|
source Z height, and leave raycasts as full 3D intersections. The Part 1 and
|
|
|
|
|
|
Part 2 licensed `Land.map` gate successfully locates sampled polygon vertices
|
|
|
|
|
|
under that contract. This closes the former XZ mismatch in terrain queries; it
|
|
|
|
|
|
does not establish object orientation, physics/gravity, original culling or the
|
|
|
|
|
|
camera matrix.
|
|
|
|
|
|
|
2026-07-18 10:08:56 +04:00
|
|
|
|
### Camera ownership boundary from the GOG renderer
|
|
|
|
|
|
|
|
|
|
|
|
The GOG `World3D.dll` export `LoadCamera` at RVA `0x1FB06` is only an import
|
|
|
|
|
|
thunk: it jumps through IAT `0x10020148` to `Terrain.dll!LoadCamera`. The
|
|
|
|
|
|
Terrain export is at RVA `0x4EBE0` and is a `__stdcall` entry with `ret 0x10`,
|
|
|
|
|
|
so it receives four machine-word arguments. It allocates a `0x1A4`-byte object,
|
|
|
|
|
|
forwards those four words to its constructor, and returns a pointer at object
|
2026-07-18 10:30:21 +04:00
|
|
|
|
offset `+0x134`. The constructor is at RVA `0x4EC60`: it passes arguments 3 and
|
|
|
|
|
|
4 to the base initialization at `this + 4`, stores argument 3 at `this + 0x138`,
|
|
|
|
|
|
and passes arguments 1, 2 and 4 plus `this` to its helper at RVA `0x4CEF0`.
|
|
|
|
|
|
Argument 1 is a NUL-terminated mode string at this call boundary: only the
|
|
|
|
|
|
embedded literals `REFLECTION` and `REFLECTION_SHIFTED` select a non-zero value
|
|
|
|
|
|
stored at `this + 0x1A0`. This proves that `LoadCamera` supports reflection
|
|
|
|
|
|
variants, but not the semantic types of its remaining arguments or the meaning
|
|
|
|
|
|
of the resulting value. `Terrain.dll!stdGetCurrentCamera2` returns a global
|
|
|
|
|
|
camera pointer, and `stdSetCurrentCamera2` updates it through the object's
|
|
|
|
|
|
interface methods.
|
2026-07-18 10:08:56 +04:00
|
|
|
|
|
|
|
|
|
|
This establishes that original camera creation/selection is Terrain-owned and
|
|
|
|
|
|
not a field that can safely be inferred from a TMA transform. It does **not**
|
|
|
|
|
|
yet establish world-to-view matrix layout, FOV, near/far values, projection
|
|
|
|
|
|
handedness, or initial mission camera selection. The Vulkan path must therefore
|
2026-07-18 10:53:03 +04:00
|
|
|
|
continue to label its XY projection as diagnostic until a dynamic capture or
|
2026-07-18 10:08:56 +04:00
|
|
|
|
further Terrain disassembly proves these contracts.
|
|
|
|
|
|
|
2026-07-18 12:47:41 +04:00
|
|
|
|
On 2026-07-18 the canonical windowed GOG process was confirmed running with
|
|
|
|
|
|
one targetable window titled `Parkan. Железная Стратегия`. Two read-only
|
|
|
|
|
|
Windows.Graphics.Capture attempts failed before returning a frame with
|
|
|
|
|
|
`SetIsBorderRequired failed: Интерфейс не поддерживается (0x80004002)`. No game
|
|
|
|
|
|
input was sent and this contributes no visual, camera, timing, or memory
|
|
|
|
|
|
evidence; a different capture/debugger path is required for dynamic analysis.
|
|
|
|
|
|
|
2026-07-18 10:12:18 +04:00
|
|
|
|
The public `World3D.dll!stdSetCurrentCamera` is a one-pointer `__stdcall`
|
|
|
|
|
|
wrapper: it delegates to the Terrain `stdSetCurrentCamera2` IAT thunk and then
|
|
|
|
|
|
writes the same pointer to a World3D mirror global. An `iron3d.dll` call site
|
|
|
|
|
|
at RVA `0x4FB95` loads the argument from an active game-state chain
|
|
|
|
|
|
`[esi + 0xBC90] + 0x94` before calling that wrapper. This proves that the
|
|
|
|
|
|
selected camera is an object reference supplied by game state, not a scalar
|
|
|
|
|
|
mission-coordinate field. The caller's enclosing type and the meaning of
|
|
|
|
|
|
offsets remain unnamed until a class layout is recovered.
|
|
|
|
|
|
|
|
|
|
|
|
Terrain strings also identify `CBufferingCamera`, `ICamera::SetTransformMatrix`
|
|
|
|
|
|
and `ICamera::GetTransformMatrix`, plus frustum/clip methods. These names prove
|
|
|
|
|
|
that a transform-matrix interface exists, but give neither method slots nor a
|
|
|
|
|
|
matrix convention; they must not be treated as a usable renderer ABI yet.
|
|
|
|
|
|
|
2026-07-18 13:34:05 +04:00
|
|
|
|
### Buffering-camera projection path from static Terrain analysis
|
|
|
|
|
|
|
|
|
|
|
|
A further headless-IDA review of the same GOG `Terrain.dll` identifies a
|
2026-07-18 13:49:15 +04:00
|
|
|
|
`CBufferingCamera`-related implementation path without requiring a live game
|
|
|
|
|
|
process. The procedure at RVA `0x4D740` copies exactly 64 bytes (16 dwords)
|
|
|
|
|
|
into its receiver at offset `+0x10`. The frame-preparation procedure at RVA
|
|
|
|
|
|
`0x4D9C0` queries a viewport rectangle through a virtual method at `+0x3C`, derives its
|
2026-07-18 13:34:05 +04:00
|
|
|
|
width, height, centre and an aspect ratio from that rectangle, and obtains
|
|
|
|
|
|
projection data through the camera interface. Projection type `0` uses a
|
2026-07-18 13:49:15 +04:00
|
|
|
|
receiver float at offset `+0x234` as an angle passed to `tan(angle / 2)`;
|
2026-07-18 13:34:05 +04:00
|
|
|
|
projection type `2` obtains a five-float block through a virtual method at
|
|
|
|
|
|
`+0x70`. Any other non-zero type raises `Not supported projection type`.
|
|
|
|
|
|
|
|
|
|
|
|
This is sufficient to prove that original viewport and projection state are
|
|
|
|
|
|
live camera inputs, rather than a fixed diagnostic projection. It does **not**
|
|
|
|
|
|
yet prove matrix row/column convention, angle units, near/far field mapping,
|
|
|
|
|
|
handedness, or the initial mission camera object. In particular, the exported
|
|
|
|
|
|
string-labelled `ICamera::SetTransformMatrix` (RVA `0x4F830`) and
|
|
|
|
|
|
`ICamera::GetTransformMatrix` (RVA `0x4F850`) are both obsolete-call stubs, so
|
|
|
|
|
|
they are not a usable method ABI.
|
|
|
|
|
|
|
2026-07-18 13:49:15 +04:00
|
|
|
|
Live read-only evidence now confirms the relocation-aware global contract. In
|
|
|
|
|
|
one elevated GOG run, `Terrain.dll` loaded at `0x02510000`; its global at
|
|
|
|
|
|
`base + 0x7355C` held non-null `0x0B37DF08`, whose first dword was
|
|
|
|
|
|
`0x025765B4` — exactly the relocated `off_100665B4` vtable from the
|
|
|
|
|
|
`LoadCamera` construction path. This proves the global is a live camera object
|
|
|
|
|
|
with that outer vtable. It also corrects an overreach: raw reads at global
|
|
|
|
|
|
offsets `+0x10` and `+0x234` did not yield a finite 4×4 matrix and a projection
|
|
|
|
|
|
angle in this sample. The static procedures' receiver and the exported global
|
|
|
|
|
|
pointer are therefore not yet proven to have the same adjustment/layout; those
|
|
|
|
|
|
offsets must remain unassigned until the selector/interface relationship is
|
|
|
|
|
|
recovered.
|
|
|
|
|
|
|
2026-07-18 13:55:17 +04:00
|
|
|
|
The unattended demo also proves that this global is dynamic. Across 25 seconds
|
|
|
|
|
|
of read-only sampling it held three distinct heap pointers; all began with the
|
|
|
|
|
|
same relocated outer vtables `0x025765B4` and `0x02576558`. Two selected
|
|
|
|
|
|
objects exposed synchronized translation triples in the paired raw blocks at
|
|
|
|
|
|
offsets `+0x2C/+0x3C/+0x4C` and `+0x6C/+0x7C/+0x8C`, with world-like values
|
|
|
|
|
|
such as `(491.562, 761.551, 7.361)`. A third object used the same vtable but
|
|
|
|
|
|
reported normalized-looking values near `(0.098, 0.018, 0.856)` and did not
|
|
|
|
|
|
match the paired block. The observed pointer transitions are consistent with
|
|
|
|
|
|
the unattended camera changes, but the sample carries no mode label and does
|
|
|
|
|
|
not prove first- versus third-person identity. It does prove that the static
|
|
|
|
|
|
direct-xref initializer is not the sole runtime writer of the global; indirect
|
|
|
|
|
|
or unanalyzed write paths remain to be recovered.
|
|
|
|
|
|
|
2026-07-18 13:59:48 +04:00
|
|
|
|
The shared outer vtable now recovers the exact transform adjustment for the
|
|
|
|
|
|
world-like samples. Its slot `+0x54` asks the subobject at `outer + 4` for
|
|
|
|
|
|
selector `0` through that subobject's slot `+0x20`, then copies three dwords
|
|
|
|
|
|
from returned offsets `+0x0C`, `+0x1C`, and `+0x2C`. In the live world-like
|
|
|
|
|
|
object, the selector field at `outer + 0x10` was `0xFFFFFFFF`; the recovered
|
|
|
|
|
|
selector implementation therefore returns `outer + 0x20`. The copied triple is
|
|
|
|
|
|
exactly `outer + 0x2C/+0x3C/+0x4C`, proving its association with the active
|
|
|
|
|
|
affine transform rather than merely a sampled correlation. The paired block
|
|
|
|
|
|
starts at `outer + 0x60` under selector `2`. Another outer slot at `+0x70`
|
|
|
|
|
|
obtains that selector-2 transform and applies `atan2` to its axis values, so it
|
|
|
|
|
|
is a proven orientation-angle path. Field names, angle order and matrix
|
|
|
|
|
|
handedness remain unassigned, but a backend-neutral camera pose may now retain
|
|
|
|
|
|
the raw affine transform and the exact translation triple without guessing them.
|
|
|
|
|
|
|
2026-07-18 14:03:18 +04:00
|
|
|
|
The render contract now represents that boundary directly. `RawCameraTransform`
|
|
|
|
|
|
preserves each selector result as sixteen original `u32` words, so diagnostic
|
|
|
|
|
|
data is not rounded or silently assigned a row/column convention. Its only
|
|
|
|
|
|
typed accessor reinterprets the proven X/Y/Z words `3/7/11` as IEEE-754
|
|
|
|
|
|
floats. `RawCameraPose` retains selector `0` and selector `2` together, and
|
|
|
|
|
|
`CameraSnapshot::raw_pose` is optional. The present Vulkan renderer continues
|
|
|
|
|
|
to use its existing view/projection matrices: raw pose capture is intentionally
|
|
|
|
|
|
not converted into a view matrix until the original transform/projection
|
|
|
|
|
|
convention has evidence.
|
|
|
|
|
|
|
2026-07-18 14:13:24 +04:00
|
|
|
|
The next live sample strengthens the representation contract but still does
|
|
|
|
|
|
not name the transform's camera-space direction. For six no-input samples of
|
|
|
|
|
|
one auto-demo camera object, selectors `0` and `2` were byte-identical,
|
|
|
|
|
|
finite affine blocks with bottom row `(0, 0, 0, 1)`. Their upper 3x3 basis
|
|
|
|
|
|
was normalized and their last-column translations changed from approximately
|
|
|
|
|
|
`(441.038, 687.481, 10.754)` to `(433.545, 652.293, 10.673)`.
|
|
|
|
|
|
|
|
|
|
|
|
This storage/order is not guessed: live `NGI32.dll!g_FastProc[23]` dispatches
|
|
|
|
|
|
to RVA `0x1D9A0`, whose SSE implementation writes each output row as the sum
|
|
|
|
|
|
of four scalar-weighted rows. Thus a contiguous 16-float block is multiplied
|
|
|
|
|
|
as a conventional row-major 4x4 matrix. The render contract now exposes a
|
|
|
|
|
|
mathematical `try_inverse_affine_row_major` helper for such finite,
|
|
|
|
|
|
non-singular `[R | t; 0 0 0 1]` blocks. It returns no result for a singular or
|
|
|
|
|
|
non-affine block and, crucially, does **not** identify that inverse as a view
|
|
|
|
|
|
matrix. Whether a selector result is camera-to-world or already a renderer
|
|
|
|
|
|
transform remains an evidence boundary.
|
|
|
|
|
|
|
2026-07-18 14:17:31 +04:00
|
|
|
|
The active camera now also has a proven live projection-interface boundary.
|
|
|
|
|
|
`LoadCamera` stores its selector-18 camera interface at outer offset `+0x19C`;
|
|
|
|
|
|
in the elevated GOG AutoDemo sample it was a `CBufferingCamera` object with
|
|
|
|
|
|
relocated vtable `0x02576344`. Its rectangle at `+0x10` was exactly
|
|
|
|
|
|
`(left=0, top=0, right=1024, bottom=768)`, its projection-type field at
|
|
|
|
|
|
`+0x230` was `0`, and its FOV field at `+0x234` was `1.04` radians.
|
|
|
|
|
|
`CBufferingCamera` passes that FOV to `tan(fov / 2)` on its type-0 projection
|
2026-07-18 14:21:48 +04:00
|
|
|
|
path. This proves the live angle input and viewport for this sample;
|
2026-07-18 14:17:31 +04:00
|
|
|
|
it does not yet assign the two remaining projection values, near/far planes,
|
|
|
|
|
|
depth range, clip-space Y direction, or a Vulkan projection matrix.
|
|
|
|
|
|
|
2026-07-18 14:21:48 +04:00
|
|
|
|
The unresolved values are now located rather than guessed. `CBufferingCamera`
|
|
|
|
|
|
uses its primary-render interface at component offset `+0x220` to select a
|
|
|
|
|
|
render context, then asks that context for a five-float projection block. The
|
|
|
|
|
|
sample's primary vtable was relocated `0x02574DF0`; its selector at `+0x0C`
|
|
|
|
|
|
is only a table lookup keyed by two still-unnamed inputs. No virtual call was
|
|
|
|
|
|
made by the probe. Therefore the block's values cannot yet be assigned as
|
|
|
|
|
|
near/far or reused by the Vulkan adapter.
|
|
|
|
|
|
|
2026-07-18 14:26:25 +04:00
|
|
|
|
The runtime render model now retains this evidence as `RawCameraProjection` on
|
|
|
|
|
|
`CameraSnapshot`: the viewport rectangle, projection selector, exact FOV bits,
|
|
|
|
|
|
and all five context words travel through the backend-neutral boundary without
|
|
|
|
|
|
changing the current Vulkan projection. Its float accessors deliberately leave
|
|
|
|
|
|
the context fields unnamed; preserving a raw ABI value is safer than encoding a
|
|
|
|
|
|
near/far or clip-space convention before it is demonstrated.
|
|
|
|
|
|
|
2026-07-18 14:32:21 +04:00
|
|
|
|
The next downstream boundary is now also identified. In the canonical static
|
|
|
|
|
|
`CBufferingCamera` vtable (`0x10066344`), slot `+0x4C` is Terrain RVA
|
|
|
|
|
|
`0x4D9C0`; it packages the camera transform, rectangle-derived viewport data,
|
|
|
|
|
|
FOV-derived tangent, and primary-render context values for another renderer
|
|
|
|
|
|
component. It does not itself assemble the final projection matrix. The GOG
|
|
|
|
|
|
`Ngi32.dll` export named `vrtSetCameraParam` resolves to a no-op compatibility
|
|
|
|
|
|
stub, while the live process loads the Direct3D7 runtime (`D3DIM700.DLL`). The
|
|
|
|
|
|
next recovery target is therefore the Direct3D transform consumer in
|
|
|
|
|
|
`iron3d.dll`, not a guessed Vulkan perspective formula. The observed `0.5`,
|
|
|
|
|
|
`700`, `0.1`, and `0.99` values remain unlabelled until that consumer proves
|
|
|
|
|
|
their roles.
|
|
|
|
|
|
|
2026-07-18 14:42:36 +04:00
|
|
|
|
### Ngi32 Direct3D7 camera matrices recovered
|
|
|
|
|
|
|
|
|
|
|
|
The downstream renderer is now concrete. `iron3d.dll` obtains an opaque
|
|
|
|
|
|
render interface through `Ngi32!niGet3DRender`; Ngi32 RVA `0x5640` returns the
|
|
|
|
|
|
global renderer object at RVA `0x3A460`. Its constructor at RVA `0x5E10` gives
|
|
|
|
|
|
the `0x8CC`-byte object vtable `0x100315E0`. During device setup, Ngi32 RVA
|
|
|
|
|
|
`0x8E70` calls the Direct3D7 device vtable at `+0x2C` (`SetTransform`) with
|
|
|
|
|
|
state `3` for the matrix made by RVA `0x7030` and state `2` for the matrix made
|
|
|
|
|
|
by RVA `0x9450`: projection then view.
|
|
|
|
|
|
|
|
|
|
|
|
RVA `0x7030` exactly builds the row-major D3D7 projection from renderer FOV
|
|
|
|
|
|
`f`, near `n`, far `z` and viewport width/height `w`/`h`:
|
|
|
|
|
|
|
|
|
|
|
|
```text
|
|
|
|
|
|
[ cos(f/2), 0, 0, 0 ]
|
2026-07-18 15:58:24 +04:00
|
|
|
|
[ 0, w/h * cos(f/2), 0, 0 ]
|
|
|
|
|
|
[ 0, 0, sin(f/2)*z/(z-n), sin(f/2) ]
|
|
|
|
|
|
[ 0, 0, -sin(f/2)*n*z/(z-n), 0 ]
|
2026-07-18 14:42:36 +04:00
|
|
|
|
```
|
|
|
|
|
|
|
2026-07-18 15:58:24 +04:00
|
|
|
|
The sine in the depth scale and homogeneous-W term is intentional: after the
|
|
|
|
|
|
D3D perspective divide the diagonal has the expected cotangent scale and the
|
|
|
|
|
|
depth range remains finite. RVA `0x9450` applies a
|
2026-07-18 14:42:36 +04:00
|
|
|
|
specific axis permutation/sign change and translated dot products to selector
|
|
|
|
|
|
0 before the view `SetTransform`; it is not merely the generic affine inverse.
|
|
|
|
|
|
|
|
|
|
|
|
An elevated read-only AutoDemo probe sampled a 1024×768 renderer with
|
|
|
|
|
|
near=`0.5`, far=`700`, FOV=`1.3` radians and showed its view-source pointer
|
|
|
|
|
|
byte-identical to the active Terrain outer camera's selector-0 block. This
|
|
|
|
|
|
proves ownership and the D3D7 boundary, while keeping the earlier
|
2026-07-18 14:53:11 +04:00
|
|
|
|
`CBufferingCamera` FOV=`1.04` as a distinct upstream interface value.
|
|
|
|
|
|
|
|
|
|
|
|
The Vulkan static path now accepts this recovered camera as
|
|
|
|
|
|
`VulkanStaticCamera`. It composes the row-major D3D7 result as
|
|
|
|
|
|
`view * projection`, uploads its 64 bytes through a vertex-stage push constant,
|
|
|
|
|
|
and changes static vertices from XY to XYZ. GLSL reads the same bytes as its
|
|
|
|
|
|
default column-major `mat4`; therefore `matrix * vec4(position, 1)` is the
|
|
|
|
|
|
transpose-equivalent of the original D3D7 row-vector multiplication. Both APIs
|
|
|
|
|
|
use the 0..1 depth range, and this renderer uses a positive viewport height, so
|
|
|
|
|
|
this narrow bridge needs no extra depth or Y correction. The fragment alpha
|
|
|
|
|
|
cutoff moved to byte offset 64 in its own push-constant range. The default
|
|
|
|
|
|
identity camera deliberately preserves the old XY diagnostic preview; feeding
|
|
|
|
|
|
live terrain camera/projection values into mission rendering remains later
|
|
|
|
|
|
runtime wiring, not a claim of scene parity.
|
2026-07-18 14:42:36 +04:00
|
|
|
|
|
2026-07-18 14:56:19 +04:00
|
|
|
|
The geometry side of that handoff is also explicit. The static mesh adapter now
|
|
|
|
|
|
has source-world projections for `Land.msh` and MSH: terrain positions retain
|
|
|
|
|
|
all three decoded coordinates; model positions retain Z and apply only the
|
|
|
|
|
|
already decoded mission translation and scale. These APIs preserve triangle
|
|
|
|
|
|
order, batch ranges and packed UV decoding, but do not interpret the still
|
|
|
|
|
|
unproven raw object orientation. The current game preview deliberately stays
|
|
|
|
|
|
on its XY diagnostic projection until a runtime supplies a real legacy camera;
|
|
|
|
|
|
selecting source-world coordinates with identity camera would be a misleading
|
|
|
|
|
|
empty/off-screen viewer rather than a compatibility result.
|
|
|
|
|
|
|
2026-07-18 15:35:00 +04:00
|
|
|
|
Static analysis of GOG `iron3d.dll` recovers the placement primitive at RVA
|
|
|
|
|
|
`0x36610`: it evaluates a row-major affine `Rz(z) * Ry(y) * Rx(x)` and writes
|
|
|
|
|
|
translation in the final column. The backend-neutral
|
|
|
|
|
|
`LegacyIron3dEulerTransform` preserves that finite portable formula with golden
|
|
|
|
|
|
yaw and scaled-point tests. A read-only elevated AutoDemo scan closes the
|
|
|
|
|
|
previous TMA binding gap for static placement: it finds the exact raw mission
|
|
|
|
|
|
records (position, `(0, 0, z)`, scale) and live object blocks at the same
|
|
|
|
|
|
world positions. For example, `z = 1.6262363` produces the observed pair
|
|
|
|
|
|
`-sin(z) = -0.99846`, `cos(z) = -0.05541`; matching pairs occur for the other
|
|
|
|
|
|
observed objects. The opt-in captured-camera path therefore applies
|
|
|
|
|
|
`R * (scale * local_position) + translation` before Vulkan upload. It remains
|
|
|
|
|
|
a static-placement contract: dynamic/animated transforms, physics ownership,
|
|
|
|
|
|
terrain material selection, lighting and gameplay parity are still separate
|
|
|
|
|
|
evidence tasks.
|
2026-07-18 15:28:20 +04:00
|
|
|
|
|
2026-07-18 14:58:51 +04:00
|
|
|
|
`VulkanSmokeRenderer::set_camera` now accepts a new finite camera between frame
|
|
|
|
|
|
submissions and uses it for the next command buffer without rebuilding the
|
|
|
|
|
|
swapchain, buffers, descriptors or pipelines. `VulkanStaticCamera` also accepts
|
|
|
|
|
|
generic finite row-major view/projection pairs, so the final game camera
|
|
|
|
|
|
controller need not be coupled to the D3D7 recovery type. The native Windows
|
|
|
|
|
|
smoke executes that update before each of 300 validation-clean frames. This is
|
|
|
|
|
|
the renderer-side cadence contract only; it does not yet supply a gameplay
|
|
|
|
|
|
camera controller or claim camera movement parity.
|
|
|
|
|
|
|
2026-07-18 15:07:31 +04:00
|
|
|
|
For controlled differential work, `fparkan-game --backend static-vulkan` now
|
|
|
|
|
|
accepts `--legacy-camera-capture <path>`. The JSON capture stores only the
|
|
|
|
|
|
selector-0 words and the already recovered Ngi32 viewport/near/far/FOV inputs;
|
|
|
|
|
|
the application reconstructs the D3D7 camera offline, selects source-world
|
|
|
|
|
|
geometry, and never attaches to or controls the original process. A live
|
|
|
|
|
|
read-only AutoDemo confirmation corrected an ABI ambiguity: the Terrain global
|
|
|
|
|
|
is the base of the `0x1A4` camera object (vtables at `+0` and `+4`), while
|
|
|
|
|
|
`LoadCamera`'s public return is the separate interface view at `base + 0x134`
|
|
|
|
|
|
with its own vtable. This capture format deliberately preserves the former
|
|
|
|
|
|
base-object interpretation and does not invent camera ownership.
|
|
|
|
|
|
|
2026-07-18 15:15:29 +04:00
|
|
|
|
The first bounded launch showed that repeated fingerprinting, rather than
|
|
|
|
|
|
Vulkan initialization, was the load-path bottleneck: each new MAT0/TEXM request
|
|
|
|
|
|
re-opened an already decoded archive and re-hashed its entire source file.
|
|
|
|
|
|
`ResourceRepository::open_archive_unchanged` now makes the *explicit* bounded
|
|
|
|
|
|
loading-transaction contract available. The normal `open_archive` remains the
|
|
|
|
|
|
strict public path and still invalidates stale entry handles after an external
|
|
|
|
|
|
archive replacement; only mission asset preparation uses the transactional
|
|
|
|
|
|
variant because it consumes an immutable snapshot of the selected game root.
|
|
|
|
|
|
|
|
|
|
|
|
With that scoped cache, canonical GOG
|
|
|
|
|
|
`MISSIONS/Autodemo.00/data.tma --backend static-vulkan --preview-roots 1`
|
|
|
|
|
|
completed preparation in 6.308 seconds (the visual graph completed in 6.012
|
|
|
|
|
|
seconds) and produced a 1280×720 native Vulkan frame with the captured D3D7
|
|
|
|
|
|
camera, 15 material descriptors, and zero validation warnings or errors. This
|
|
|
|
|
|
is a real original-asset rendering milestone, but not pixel parity or gameplay
|
|
|
|
|
|
camera parity: it intentionally renders the bounded first preview root and
|
|
|
|
|
|
uses a one-time offline camera capture.
|
2026-07-18 15:07:31 +04:00
|
|
|
|
|
2026-07-18 15:19:07 +04:00
|
|
|
|
Expanding that same preview to all eight AutoDemo mission objects exposed a
|
|
|
|
|
|
different, renderer-local limit: the merged static scene crossed 65,535
|
|
|
|
|
|
vertices before any Vulkan command was recorded. `VulkanStaticMesh` now uses a
|
|
|
|
|
|
32-bit GPU index buffer (`VK_INDEX_TYPE_UINT32`), while source MSH/TerrainFace28
|
|
|
|
|
|
indices remain decoded in their original 16-bit representations and are widened
|
|
|
|
|
|
only at the static-render bridge. A regression test crosses the former boundary.
|
|
|
|
|
|
The full captured-camera AutoDemo preview completed at 13.369 seconds and
|
|
|
|
|
|
rendered three 1280×720 native frames: 8 mission objects, 66 mesh components,
|
2026-07-18 15:35:00 +04:00
|
|
|
|
67 material descriptors, and zero Vulkan validation warnings or errors. A later
|
|
|
|
|
|
orientation-enabled run rendered the same full scene for three 1280×720 frames
|
|
|
|
|
|
with `validation_warnings=0`, `validation_errors=0` and readback hash
|
|
|
|
|
|
`6406107678925513509`. It is still a static source-world preview: terrain
|
|
|
|
|
|
material selection, lighting, animation, physics and gameplay behavior have
|
|
|
|
|
|
not yet been claimed as parity.
|
2026-07-18 15:19:07 +04:00
|
|
|
|
|
2026-07-18 13:38:49 +04:00
|
|
|
|
A fresh no-input launch of the canonical `iron_3d.exe` did create a responsive
|
|
|
|
|
|
window titled `Parkan. Железная Стратегия`. A read-only probe then requested
|
|
|
|
|
|
`PROCESS_QUERY_INFORMATION | PROCESS_VM_READ` and attempted to read the known
|
|
|
|
|
|
Terrain global at image address `0x1007355C`; `OpenProcess` returned
|
|
|
|
|
|
`ERROR_ACCESS_DENIED` (5), so no process memory or camera value was read. The
|
|
|
|
|
|
window was left running. A same- or higher-integrity 32-bit debugger is still
|
|
|
|
|
|
required for live camera evidence.
|
|
|
|
|
|
|
2026-07-18 08:46:16 +04:00
|
|
|
|
`Land.msh` использует отдельный geometry-only bridge: validated `TerrainFace28`
|
|
|
|
|
|
сохраняет source triangle order, а его positions и packed UV0 попадают в тот же
|
2026-07-18 10:53:03 +04:00
|
|
|
|
static vertex/index upload path. Для текущего диагностического viewer XY bounds
|
2026-07-18 08:46:16 +04:00
|
|
|
|
нормализуются в clip-space, packed UV0 декодируется как signed `int16 / 1024`.
|
|
|
|
|
|
Один static draw range намеренно не трактует `material_tag`, surface mask, slots
|
|
|
|
|
|
или auxiliary streams как material/pipeline state: их связь с original terrain
|
|
|
|
|
|
renderer пока не доказана. GOG `DATA/MAPS/11/Land.msh` дал 6 458 vertices и
|
|
|
|
|
|
24 672 indices в 300-frame native Vulkan run без validation warning/error.
|
|
|
|
|
|
|
2026-07-18 08:31:19 +04:00
|
|
|
|
Выбор depth/stencil attachment отделён от renderer lifetime:
|
2026-07-18 08:26:37 +04:00
|
|
|
|
`select_depth_stencil_attachment_format` применяет тот же фиксированный порядок
|
|
|
|
|
|
форматов, что и capability gate, к фактически поддерживаемому списку GPU.
|
|
|
|
|
|
Это исключает ситуацию, когда admission принимает один совместимый формат, а
|
2026-07-18 08:31:19 +04:00
|
|
|
|
allocation позднее выбирает другой; resource уничтожается после framebuffer и
|
|
|
|
|
|
render pass при recreate/teardown.
|
2026-07-18 08:26:37 +04:00
|
|
|
|
|
2026-06-22 01:58:51 +04:00
|
|
|
|
После последнего world pass renderer закрывает сцену и выводит back buffer.
|
|
|
|
|
|
World3D снимает `in_render`, восстанавливает временный viewport state и вызывает
|
|
|
|
|
|
`on_end_render` у active objects. Только после этого допустимо освобождать
|
|
|
|
|
|
temporary vertex buffers или заменять render representation. UI/shell
|
|
|
|
|
|
обслуживается верхним уровнем после возврата из world-render path; для
|
|
|
|
|
|
диагностики полезно уметь сохранять world-only command list и финальный
|
|
|
|
|
|
framebuffer отдельно.
|
|
|
|
|
|
|
2026-07-18 16:36:25 +04:00
|
|
|
|
Terrain material discovery is now reproducible instead of inferred from the
|
|
|
|
|
|
mesh shape. The original AutoDemo map places two textual sidecar WEAR tables,
|
|
|
|
|
|
`Land1.wea` and `Land2.wea`, beside `Land.msh`; `Terrain.dll` contains the
|
|
|
|
|
|
corresponding `1.wea` and `2.wea` loading suffixes. The new terrain inspector
|
|
|
|
|
|
reports all packed face tags. For `DATA/MAPS/AutoMAP/Land.msh`, its 3,174 faces
|
|
|
|
|
|
use `0x0102` (592), `0x0203` (386), `0xff01` (918), `0xff02` (970), and
|
|
|
|
|
|
`0xff03` (308). The low byte is always a valid positional selector 1..3 in
|
|
|
|
|
|
`Land2.wea`; the high byte is a `Land1.wea` selector or sentinel `0xff`.
|
|
|
|
|
|
This is evidence for a two-layer terrain-material contract, but not yet for
|
|
|
|
|
|
its blend equation, so the Vulkan bridge intentionally remains texture-agnostic
|
|
|
|
|
|
until the base/overlay composition is recovered.
|
|
|
|
|
|
|
2026-07-18 16:52:46 +04:00
|
|
|
|
Static recovery of `Terrain.dll!CLandscape` now fixes the loader contract behind
|
|
|
|
|
|
that observation. The constructor opens `Land.msh`, creates one material manager
|
|
|
|
|
|
from `Land1.wea`, then invokes that manager's second-table load with `Land2.wea`.
|
|
|
|
|
|
It requires NRes type 18 with the original diagnostic “microtexture mapping
|
|
|
|
|
|
chunk” and retains it as four-byte entries; type 14 is optional. Therefore the
|
|
|
|
|
|
two WEAR selectors must not be collapsed into an invented one-layer material,
|
|
|
|
|
|
and the type-18 `aux18` stream is now documented as microtexture mapping data.
|
|
|
|
|
|
`TerrainFace28::material_layers` exposes the evidenced high-byte `Land1`
|
|
|
|
|
|
selector (with `0xff` absent sentinel) and low-byte `Land2` selector. The
|
2026-07-18 17:01:17 +04:00
|
|
|
|
manager's actual blend/pass operation remains unrecovered. `World3D.dll`
|
|
|
|
|
|
supplies the manager ABI: its load slot appends `Land1` as table 0 and `Land2`
|
|
|
|
|
|
as table 1, while its material-phase lookup receives the exact 32-bit selector
|
|
|
|
|
|
`(table_index << 16) | material_index`. `TerrainMaterialSelection` preserves
|
|
|
|
|
|
that key for the renderer without assuming how the two phases are blended.
|
2026-07-18 16:52:46 +04:00
|
|
|
|
|
2026-07-18 17:07:34 +04:00
|
|
|
|
The same `World3D!GetMaterialPhase` recovery establishes the next boundary. It
|
|
|
|
|
|
validates the table/row selector, chooses a MAT0 phase (or interpolates two
|
|
|
|
|
|
phases for an animated material), returns the selected material record, and
|
|
|
|
|
|
hands its caller a 76-byte expanded phase-state block. It does not submit a
|
|
|
|
|
|
draw or compose the two terrain tables. The later consumer of that record and
|
|
|
|
|
|
the type-18 microtexture mapping remains the required evidence before adding
|
|
|
|
|
|
an overlay, blend, or microtexture shader.
|
|
|
|
|
|
|
2026-07-18 17:11:05 +04:00
|
|
|
|
The terrain render traversal is now located at `Terrain.dll` RVA `0x11340`.
|
|
|
|
|
|
After visibility/cell work, it derives a 68-byte source-material record and
|
|
|
|
|
|
dispatches renderer-context slot `+16` with four arguments: zero, the record's
|
|
|
|
|
|
16-bit word at `+2`, a pointer selected from a four-byte table by its word at
|
|
|
|
|
|
`+0`, and zero. A preceding renderer-context slot `+60` receives the current
|
|
|
|
|
|
visibility mode. This is the concrete terrain-to-renderer handoff; it does not
|
|
|
|
|
|
call `GetMaterialPhase` directly and does not yet identify either word, the
|
|
|
|
|
|
four-byte payload, or the final blend operation.
|
|
|
|
|
|
|
2026-07-18 16:41:53 +04:00
|
|
|
|
The static Vulkan bridge now carries each contiguous face run as a separate
|
|
|
|
|
|
draw range keyed by the original packed `material_tag`; it neither reorders
|
|
|
|
|
|
triangles nor collapses the high byte. For the first usable visual layer, the
|
|
|
|
|
|
low byte resolves positionally through the adjacent standalone `Land2.wea`,
|
|
|
|
|
|
then the existing `MAT0 -> Textures.lib -> TEXM` chain supplies the RGBA8
|
|
|
|
|
|
descriptor. `Land1.wea` remains preserved as the high byte/overlay channel and
|
|
|
|
|
|
is deliberately not drawn until its blend equation is recovered. A canonical
|
|
|
|
|
|
three-frame AutoDemo run now uses 71 descriptors (66 MSH plus five terrain
|
|
|
|
|
|
tags), produces readback hash `17034026600547661445`, and stays at zero Vulkan
|
|
|
|
|
|
validation warnings/errors. This is original terrain texture upload, not
|
|
|
|
|
|
terrain visual parity.
|
|
|
|
|
|
|
2026-07-18 16:36:25 +04:00
|
|
|
|
```powershell
|
|
|
|
|
|
cargo run -q -p fparkan-cli -- terrain inspect `
|
|
|
|
|
|
'C:\GOG Games\Parkan - Iron Strategy\DATA\MAPS\AutoMAP\Land.msh' --format json
|
|
|
|
|
|
```
|
|
|
|
|
|
|
2026-06-22 01:58:51 +04:00
|
|
|
|
## Проверки паритета
|
|
|
|
|
|
|
|
|
|
|
|
Главные риски совпадения кадра:
|
|
|
|
|
|
|
|
|
|
|
|
- x87 extended precision и правила округления;
|
|
|
|
|
|
- различия scalar/SIMD slots `g_FastProc`;
|
|
|
|
|
|
- порядок objects, batches и transparent primitives;
|
|
|
|
|
|
- depth write/test, cull, alpha test и blend transitions;
|
|
|
|
|
|
- mip-skip, palette и `Page` coordinates;
|
|
|
|
|
|
- material fallback и выбор phase;
|
|
|
|
|
|
- последовательность RNG для FX и atmosphere;
|
|
|
|
|
|
- capability fallback конкретного устройства;
|
|
|
|
|
|
- quantization времени и дополнительный simulation step;
|
|
|
|
|
|
- eager/lazy resource resolve и cache side effects.
|
|
|
|
|
|
|
|
|
|
|
|
Минимальный deterministic frame capture должен включать camera state, viewport,
|
|
|
|
|
|
visible object IDs, выбранные LOD/group/slot, draw-item list, material и texture
|
|
|
|
|
|
handles, pipeline keys, matrices, render phase, sort key, причины culling и
|
|
|
|
|
|
hashes промежуточных buffers. Без такой трассировки нельзя уверенно отделить
|
|
|
|
|
|
ошибку формата MSH от ошибки state machine renderer-а или сортировки.
|
|
|
|
|
|
|
|
|
|
|
|
Связанные справочные страницы с таблицами форматов: [MSH](../reference/msh.md),
|
|
|
|
|
|
[materials](../reference/materials.md), [Texm](../reference/texm.md) и
|
|
|
|
|
|
[render frame](../reference/render-frame.md).
|
2026-07-18 15:50:52 +04:00
|
|
|
|
|
2026-07-18 15:58:24 +04:00
|
|
|
|
### Live AutoDemo: empty frame устранён, но parity ещё не достигнут
|
2026-07-18 15:50:52 +04:00
|
|
|
|
|
|
|
|
|
|
Read-only `PrintWindow` capture работающего GOG AutoDemo даёт полноценный
|
|
|
|
|
|
оригинальный кадр (terrain, варбот и близкая geometry), поэтому теперь есть
|
|
|
|
|
|
безвводный source для будущего visual comparison. Текущий полный static-Vulkan
|
2026-07-18 15:58:24 +04:00
|
|
|
|
preview раньше состоял только из clear color. Validation `0/0` тогда доказывал
|
|
|
|
|
|
только GPU lifecycle, но не совпадение с original renderer.
|
2026-07-18 15:50:52 +04:00
|
|
|
|
|
|
|
|
|
|
Новая CPU-диагностика применяет к тем же source vertices ту же row-major D3D7
|
|
|
|
|
|
matrix, которую GLSL получает как column-major push constant. Для свежего
|
|
|
|
|
|
captured Ngi32 camera (`viewport 1024x768`, near `5`, far `700`, FOV `1.3`)
|
2026-07-18 15:58:24 +04:00
|
|
|
|
она насчитала `clip_visible_vertices=0`. Последующая сверка с
|
|
|
|
|
|
`Ngi32!sub_10009450` и `sub_10007030` нашла два literal translation defects:
|
|
|
|
|
|
Y translation view matrix должна быть `-(m23*m22 + m13*m12 + m03*m02)`, а
|
|
|
|
|
|
projection использует `width/height` и множитель `sin(fov/2)` в обоих depth
|
|
|
|
|
|
coefficients. После исправления того же capture диагностировал
|
|
|
|
|
|
`clip_visible_vertices=938`, а `PrintWindow` native Vulkan window показал
|
|
|
|
|
|
реальную rasterized terrain/model geometry. Это исключает прежний empty-frame
|
|
|
|
|
|
барьер; face culling и texture selection не были его причиной.
|
|
|
|
|
|
|
|
|
|
|
|
Получившийся кадр всё ещё явно не похож на оригинал: geometry сосредоточена у
|
|
|
|
|
|
границы и material/terrain representation грубая. Значит, следующие задачи —
|
|
|
|
|
|
camera timing/selection, source model-node transforms, terrain material phases,
|
|
|
|
|
|
lighting и visibility, а не объявление pixel parity.
|
2026-07-18 15:50:52 +04:00
|
|
|
|
|
2026-07-18 16:02:31 +04:00
|
|
|
|
MSH static bridge теперь также перестал отправлять каждый `Batch20` файла.
|
|
|
|
|
|
Для стандартного `Node38` он выбирает `slot_index[LOD0, group0]`, затем
|
|
|
|
|
|
соответствующий `Slot68.batch_start..batch_count`; alternate LOD/groups больше
|
|
|
|
|
|
не подмешиваются в initial static pose. Regression test фиксирует selection,
|
|
|
|
|
|
а fresh AutoDemo run меняет readback hash при тех же 938 clip-visible vertices
|
|
|
|
|
|
и остаётся validation-clean. Новый capture по-прежнему показывает сильное
|
|
|
|
|
|
смещение частей, поэтому одной selection недостаточно: нужны local node poses,
|
|
|
|
|
|
parent hierarchy и animation-key/fallback semantics. Модели без layout Node38
|
|
|
|
|
|
сохраняют прежний all-batches diagnostic fallback.
|
|
|
|
|
|
|
2026-07-18 16:08:18 +04:00
|
|
|
|
Чтобы следующий шаг не перечитывал raw NRes streams в renderer, validated
|
|
|
|
|
|
`ModelAsset` теперь переносит `ModelAnimation` с decoded type-8 keys, type-19
|
|
|
|
|
|
map и declared frame count. `node38_fallback_pose` возвращает exact fallback
|
2026-07-18 16:25:20 +04:00
|
|
|
|
key pose. Licensed gates Part 1/Part 2 подтвердили 435/511 models, 157/200
|
|
|
|
|
|
animated models, 3,469/5,233 node samples и утверждённые captures.
|
2026-07-18 16:08:18 +04:00
|
|
|
|
|
2026-07-18 16:14:58 +04:00
|
|
|
|
Следующий узкий шаг применяет этот уже декодированный `fallback_key` в
|
|
|
|
|
|
legacy-camera static preview. Для каждого выбранного `Node38` bridge поворачивает
|
|
|
|
|
|
вершины normalized quaternion, прибавляет local translation pose и только затем
|
|
|
|
|
|
применяет доказанный TMA `Rz * Ry * Rx`, scale и mission translation. Вершины
|
|
|
|
|
|
намеренно разворачиваются per node/draw range, чтобы один source vertex мог
|
2026-07-18 16:25:20 +04:00
|
|
|
|
получить разные poses. `parent_or_link_raw == 0xFFFF` теперь доказан как root,
|
|
|
|
|
|
а любое меньшее child значение — parent index: inspected `R_B_01.msh` содержит
|
|
|
|
|
|
34-node parent-before-child tree, а Part 1/Part 2 animation gate подтвердил
|
|
|
|
|
|
этот контракт для всех standard nodes. Parent rotation корректно поворачивает
|
|
|
|
|
|
child translation до суммирования. Frame map всё ещё не семплируется, поэтому
|
|
|
|
|
|
это static fallback hierarchy, а не full animation parity.
|
2026-07-18 16:14:58 +04:00
|
|
|
|
На той же GOG `MISSIONS/Autodemo.00/data.tma` и offline Ngi32 camera capture
|
|
|
|
|
|
full preview из 8 objects / 66 mesh components / 67 descriptors завершился за
|
2026-07-18 16:25:20 +04:00
|
|
|
|
один native Vulkan run с `clip_visible_vertices=3415`, `validation_warnings=0`,
|
|
|
|
|
|
`validation_errors=0` и readback hash `1275533143935640133`. Рост с 2584
|
|
|
|
|
|
видимых вершин подтверждает, что composed parent poses реально вошли в geometry
|
2026-07-18 16:14:58 +04:00
|
|
|
|
path; он не является оценкой сходства с original frame.
|
|
|
|
|
|
|
2026-07-18 15:50:52 +04:00
|
|
|
|
Параллельно bridge для legacy-camera path теперь переводит только высоту
|
|
|
|
|
|
`Land.msh` в world units с масштабом `1/32`: raw AutoDemo heights
|
|
|
|
|
|
`95.29412..288.23532` становятся `2.978..9.007`, что согласуется с TMA Z и
|
|
|
|
|
|
live camera height. Это доказанное coordinate conversion, но оно само по себе
|
|
|
|
|
|
не сделало вершины видимыми и не является заявлением о pixel parity.
|