diff --git a/OPENXR.md b/OPENXR.md
index 1706fce..f563cd3 100644
--- a/OPENXR.md
+++ b/OPENXR.md
@@ -62,6 +62,7 @@ steering_wheel = true
native_steering_wheel = true
object_culling = false
hand_steering = true
+cockpit_item_hand = "left"
performance_level = "boost"
```
@@ -300,6 +301,19 @@ see [Steering wheel and hand
steering](#steering-wheel-and-hand-steering). Turning the wheel moves the controllers, and the game's
own motion detection still reads them, so a sharp enough turn can read as a shake.
+**Held item.** `cockpit_item_hand` accepts `"left"` (default), `"right"`, or `"off"` and is also
+available in F10 > VR. In cockpit view, the selected tracked hand holds one item model from the
+game's `Race/Common.szs` after the roulette settles. The item stands upright just above the palm
+with its front toward the player. It turns only with the hand's heading, so rolling or tilting the
+hand never tips it over. Triple items show their remaining inventory count beside the model, facing
+the player. The display follows player 1's inventory: using, losing, or deploying the
+item removes it from the hand even if a deployed object remains near the kart. Stick steering and
+the existing item buttons still work. The imported models use their static bind pose; item effects
+and animations are not reproduced in the hand. Each material is drawn from its own data: texture
+layers with their wrap modes, SRT and environment mapping, vertex colours, culling, blending and up
+to four TEV stages. Only the lighting is approximated, by a fixed cockpit light in place of the
+course's light set.
+
**Bare hands.** On the Quest, with `hand_tracking` on and the controllers put down, the hands drive
`khr/simple_controller`: a right pinch is A with the pointer on the hand's aim ray, the left
palm-up pinch is + (pause), and in the cockpit, while a hand holds the wheel, that hand holds A and
diff --git a/android/app/src/main/java/org/wiicompiled/quest/launcher/SettingsPage.kt b/android/app/src/main/java/org/wiicompiled/quest/launcher/SettingsPage.kt
index 220bf9e..31cecd8 100644
--- a/android/app/src/main/java/org/wiicompiled/quest/launcher/SettingsPage.kt
+++ b/android/app/src/main/java/org/wiicompiled/quest/launcher/SettingsPage.kt
@@ -189,13 +189,20 @@ class SettingsPage(
write = { c, value -> c.setBool("vr", "hand_steering", value) },
enabledIf = cockpit,
)
- // The hands follow the headset's hand tracking; kVrHandTrackingDefault is off. The
- // hands are only drawn while they can steer, so it goes with hand steering.
+ choice(
+ R.string.vr_cockpit_item_hand, R.string.vr_cockpit_item_hand_helper,
+ listOf(R.string.vr_item_left, R.string.vr_item_right, R.string.vr_item_off),
+ read = { stringIndex(it, "vr", "cockpit_item_hand", ITEM_HANDS) },
+ write = { c, index -> c.setString("vr", "cockpit_item_hand", ITEM_HANDS[index]) },
+ enabledIf = cockpit,
+ )
+ // Tracked hands can show the item with stick steering as well.
toggle(
R.string.vr_hand_tracking, R.string.vr_hand_tracking_helper,
read = { it.bool("vr", "hand_tracking") ?: false },
write = { c, value -> c.setBool("vr", "hand_tracking", value) },
- enabledIf = { c -> cockpit(c) && (c.bool("vr", "hand_steering") ?: true) },
+ enabledIf = { c -> cockpit(c) && ((c.bool("vr", "hand_steering") ?: true) ||
+ stringIndex(c, "vr", "cockpit_item_hand", ITEM_HANDS) != 2) },
)
slider(
R.string.vr_lean_back, R.string.vr_lean_back_helper, -45.0, 45.0, 1.0,
@@ -755,6 +762,7 @@ class SettingsPage(
val ROTATION_DEFAULT = ROTATIONS.indexOf("yaw_pitch")
// The runtime's default ("cockpit") first.
val SEATS = listOf("cockpit", "custom")
+ val ITEM_HANDS = listOf("left", "right", "off")
// The runtime's default ("boost") first: an absent key reads as index 0.
val PERFORMANCE_LEVELS = listOf("boost", "sustained_high", "sustained_low", "power_savings", "default")
// runtime_config.h's kVrFoveationLevels, and its Quest default.
diff --git a/android/app/src/main/res/values/strings.xml b/android/app/src/main/res/values/strings.xml
index eb1aa05..68f2d21 100644
--- a/android/app/src/main/res/values/strings.xml
+++ b/android/app/src/main/res/values/strings.xml
@@ -535,6 +535,11 @@
Custom
Hand steering
In the cockpit, squeeze a grip near the steering wheel or handlebar to grab it, and turn it to steer. Releasing both grips gives steering back to the stick. Hand steering by heurazy.
+ Item in cockpit hand
+ Show Player 1\'s settled inventory item above the selected palm. Triple items show the remaining count. Using or losing the item hides it.
+ Left
+ Right
+ Off
Tracked hands
The cockpit hands follow your own. Holding the controllers, the fingers follow their touch sensors; put one down and the cameras track that hand at once. Put the controllers down to drive with bare hands: close a hand on the wheel to hold it, which also holds the gas; pinch with a free hand to use an item; flick your hands up for a trick; pinch with your left palm facing you to pause. In menus, a right pinch is A. Choose Automatic drift. Needs hand tracking on in the headset\'s settings.
Lean back angle
diff --git a/aurora-main/include/aurora/aurora.h b/aurora-main/include/aurora/aurora.h
index 7510ece..1c75b71 100644
--- a/aurora-main/include/aurora/aurora.h
+++ b/aurora-main/include/aurora/aurora.h
@@ -153,6 +153,16 @@ typedef struct {
AuroraCockpitHand hands[2];
} AuroraCockpit;
+// Inventory from the same guest frame as the scene. hand: 0 left, 1 right,
+// 2 off. A zero-initialised value has no item.
+typedef struct {
+ uint64_t raceGeneration;
+ uint8_t id;
+ uint8_t count;
+ uint8_t hand;
+ bool valid;
+} AuroraCockpitItem;
+
typedef struct {
float position[3];
int16_t joints[4];
@@ -324,6 +334,11 @@ void aurora_set_stereo_scene_anchor(const float anchorFromScene[12]);
// The sealed frame then owns that scale: each eye's head/IPD translation is
// rescaled from the packet's AuroraCockpit::unitsPerMeter to it.
void aurora_set_stereo_scene_anchor_scaled(const float anchorFromScene[12], float unitsPerMeter);
+// GX producer thread, after the scene anchor and before sealing that frame.
+void aurora_set_stereo_cockpit_item(const AuroraCockpitItem* item);
+// Copies a user-supplied Race/Common.szs archive. May be called on the guest
+// thread; the renderer owns decoded assets and never refers back to guest RAM.
+void aurora_set_cockpit_item_archive(const void* bytes, uint32_t size);
// Select Player 1's subview for immersive replay of 2-4 local screens.
// Producer-thread, per-frame metadata, consumed by the next end_frame call.
// One (the default) keeps full-frame replay. Desktop rendering is unaffected.
diff --git a/aurora-main/lib/aurora.cpp b/aurora-main/lib/aurora.cpp
index d87e856..7f8b00f 100644
--- a/aurora-main/lib/aurora.cpp
+++ b/aurora-main/lib/aurora.cpp
@@ -118,6 +118,7 @@ struct StereoSceneAnchor {
// World units per metre the anchor was built with, or zero when the packet's
// own scale applies (aurora_set_stereo_scene_anchor_scaled).
float unitsPerMeter = 0.f;
+ AuroraCockpitItem cockpitItem{};
};
// Producer thread only, between aurora_set_stereo_scene_anchor() and the seal
// that consumes it. Cleared at every seal so a producer that stops publishing
@@ -829,6 +830,7 @@ gfx::StereoReplayFrame make_stereo_replay_frame(const AuroraStereoFrame& input,
std::memcpy(&anchorFromScene, sceneAnchor.anchorFromScene.data(), sizeof(anchorFromScene));
gfx::StereoReplayFrame replay{};
replay.cockpit = input.cockpit;
+ replay.cockpitItem = sceneAnchor.cockpitItem;
replay.window = input.mode == AURORA_STEREO_FRAME_IMMERSIVE_REPLAY && input.window;
// The sealed guest frame owns its scale. The packet may have been sampled
// just before a change of scale (a character swap, a lightning strike), so
@@ -2703,6 +2705,10 @@ void set_stereo_scene_anchor(const float anchorFromScene[12]) noexcept {
g_pendingSceneAnchor = anchor;
}
+void set_stereo_cockpit_item(const AuroraCockpitItem* item) noexcept {
+ g_pendingSceneAnchor.cockpitItem = item != nullptr ? *item : AuroraCockpitItem{};
+}
+
#ifdef AURORA_ENABLE_GX
namespace stereo {
void set_sink(SinkCallback callback, SubmitCallback submitted, void* userdata) noexcept {
@@ -2763,6 +2769,10 @@ void aurora_set_stereo_scene_anchor_scaled(const float anchorFromScene[12], floa
aurora::g_pendingSceneAnchor.unitsPerMeter = unitsPerMeter;
}
}
+
+void aurora_set_stereo_cockpit_item(const AuroraCockpitItem* item) {
+ aurora::set_stereo_cockpit_item(item);
+}
void aurora_set_stereo_local_player_count(uint32_t count) {
aurora::g_pendingStereoLocalPlayerCount = count >= 1 && count <= 4 ? count : 1;
}
diff --git a/aurora-main/lib/gfx/cockpit.hpp b/aurora-main/lib/gfx/cockpit.hpp
index 5dc2ae7..41a7838 100644
--- a/aurora-main/lib/gfx/cockpit.hpp
+++ b/aurora-main/lib/gfx/cockpit.hpp
@@ -7,6 +7,7 @@
// and hand steering".
#pragma once
#include "common.hpp"
+#include "cockpit_item.hpp"
#include "../webgpu/gpu.hpp"
#include
#include
@@ -251,12 +252,44 @@ inline void build_geometry(const AuroraCockpit& cockpit, std::vector& ve
else glove(vertices,hand,side);
}
}
+inline void append_item_badge(const AuroraCockpit& cockpit,const AuroraCockpitItem& item,
+ std::vector& vertices) {
+ if(!item.valid || item.hand>1 || item.count<1 || item.count>3 ||
+ (item.id!=5 && item.id!=16 && item.id!=17 && item.id!=18) ||
+ !cockpit.hands[item.hand].tracked || !cockpit_item::has_model(item.id)) return;
+ const auto& hand=cockpit.hands[item.hand];
+ if(hand.jointsValid && !joints_finite(hand)) return;
+ M frame;
+ if(!cockpit_item::seat_from_item(hand,frame)) return;
+ // In the item's upright frame, beside the widest model on the hand's outer
+ // side, facing the player like the item.
+ const float side=item.hand==0?-1.0f:1.0f;
+ const size_t first=vertices.size();
+ const V base{side*0.095f,0.035f,0.0f};
+ ellipsoid(vertices,base,{0.016f,0.020f,0.004f},{0.05f,0.08f,0.13f});
+ // A tiny raised seven-segment digit stays legible without creating another
+ // textured game asset. One model is held for every triple inventory ID.
+ const uint8_t digit=item.count==1?0x06:item.count==2?0x5b:0x4f;
+ const V white{0.95f,0.98f,0.85f};
+ const auto segment=[&](int bit,float x0,float y0,float x1,float y1) {
+ if(digit&(1u< geometry(const AuroraCockpit& cockpit) {
std::vector result;build_geometry(cockpit,result);return result;
}
inline std::atomic meshRevision{1};
inline std::vector frameVertices;
inline AuroraCockpit cachedCockpit{};
+inline AuroraCockpitItem cachedCockpitItem{};
inline uint64_t cachedMeshRevision=0;
inline wgpu::RenderPipeline pipeline;
struct SceneDepth {
@@ -268,7 +301,7 @@ inline bool pipelineReversedDepth=false;
inline wgpu::TextureFormat pipelineFormat{}, pipelineDepthFormat{};
inline std::array vertexBuffers;
inline std::array vertexCapacity{};
-inline void shutdown() { pipeline=nullptr;pipelineSamples=0;vertexBuffers={};vertexCapacity={};cachedMeshRevision=0;frameVertices.clear(); }
+inline void shutdown() { pipeline=nullptr;pipelineSamples=0;vertexBuffers={};vertexCapacity={};cachedMeshRevision=0;cachedCockpitItem={};frameVertices.clear();cockpit_item::shutdown(); }
inline void render(wgpu::CommandEncoder& cmd,const StereoReplayFrame& frame,uint32_t eye,SceneDepth sceneDepth={},
const wgpu::RenderPassEncoder* existingPass=nullptr) {
if(!frame.cockpit.active || !sceneDepth.valid) return;
@@ -308,9 +341,11 @@ inline void render(wgpu::CommandEncoder& cmd,const StereoReplayFrame& frame,uint
pipelineReversedDepth=reversedDepth;pipelineDepthFormat=target.depthFormat;
}
const auto revision=meshRevision.load();
- if(cachedMeshRevision!=revision || std::memcmp(&cachedCockpit,&frame.cockpit,sizeof(AuroraCockpit))!=0) {
+ if(cachedMeshRevision!=revision || std::memcmp(&cachedCockpit,&frame.cockpit,sizeof(AuroraCockpit))!=0 ||
+ std::memcmp(&cachedCockpitItem,&frame.cockpitItem,sizeof(AuroraCockpitItem))!=0) {
build_geometry(frame.cockpit,frameVertices);
- cachedCockpit=frame.cockpit;cachedMeshRevision=revision;
+ append_item_badge(frame.cockpit,frame.cockpitItem,frameVertices);
+ cachedCockpit=frame.cockpit;cachedCockpitItem=frame.cockpitItem;cachedMeshRevision=revision;
}
const auto& vertices=frameVertices;
if(vertices.empty()) return;
@@ -348,6 +383,7 @@ inline void render(wgpu::CommandEncoder& cmd,const StereoReplayFrame& frame,uint
// Mark only depth-visible samples; later virtual-screen draws test for zero.
pass.SetStencilReference(1);
pass.SetPipeline(pipeline);pass.SetVertexBuffer(0,buffer);pass.Draw(clip.size());
+ cockpit_item::render(pass,frame,eye,sceneDepth.z,sceneDepth.constant);
pass.SetStencilReference(0);
if(!existingPass) pass.End();
}
diff --git a/aurora-main/lib/gfx/cockpit_item.hpp b/aurora-main/lib/gfx/cockpit_item.hpp
new file mode 100644
index 0000000..6f7602d
--- /dev/null
+++ b/aurora-main/lib/gfx/cockpit_item.hpp
@@ -0,0 +1,528 @@
+// SPDX-License-Identifier: GPL-3.0-or-later
+#pragma once
+
+#include "cockpit_item_data.hpp"
+#include "texture_convert.hpp"
+#include "../webgpu/gpu.hpp"
+
+#include
+#include
+#include
+#include
+#include
+
+namespace aurora::gfx::cockpit_item {
+
+inline std::mutex archiveMutex;
+inline std::shared_ptr archive;
+inline std::atomic archiveRevision{0};
+
+inline size_t mip_bytes(const data::Texture& texture,uint32_t mips) {
+ size_t total=0;
+ for(uint32_t level=0;level>level,1))*std::max(texture.height>>level,1)*4;
+ return total;
+}
+inline void set_archive(const void* bytes,uint32_t size) {
+ { std::lock_guard lock(archiveMutex); if(archive) return; }
+ auto parsed=std::make_shared(data::parse_archive(bytes,size));
+ for(auto& model:parsed->models) for(auto& texture:model.textures) {
+ for(uint32_t mips:{texture.mips,1u}) {
+ auto converted=convert_texture(texture.format,texture.width,texture.height,mips,
+ ArrayRef(texture.bytes));
+ const size_t length=mip_bytes(texture,mips);
+ if(converted.format!=wgpu::TextureFormat::RGBA8Unorm || converted.data.size()loaded?std::move(parsed):nullptr;
+ ++archiveRevision;
+}
+inline bool has_model(uint8_t id) {
+ const int index=data::model_index(id);
+ if(index<0) return false;
+ std::lock_guard lock(archiveMutex);
+ return archive && archive->models[index].valid();
+}
+
+// Uniform images. WGSL: struct Material and struct Frame below.
+struct GpuStage { uint32_t color[4],colorOp[4],alpha[4],alphaOp[4],misc[4];float konst[4],texGen[2][4]; };
+struct GpuMaterial { GpuStage stages[4];float registers[4][4],materialColor[4];uint32_t info[4]; };
+static_assert(sizeof(GpuMaterial)==608);
+struct GpuFrame { float eyeFromModel[12],seatFromModel[12],projection[4],depth[4]; };
+static_assert(sizeof(GpuFrame)==128);
+// Static per-model vertices: billboards keep their origin in position (w=1)
+// and their bone-local offset, which the vertex shader turns to face the eye.
+struct GpuVertex { float position[4],offset[4],normal[4],color[4],uv[4]; };
+static_assert(sizeof(GpuVertex)==80);
+
+inline GpuMaterial gpu_material(const data::Material& material) {
+ GpuMaterial out{};
+ for(uint32_t i=0;i>12)&15u;g.color[1]=(c>>8)&15u;g.color[2]=(c>>4)&15u;g.color[3]=c&15u;
+ g.colorOp[0]=(c>>16)&3u;g.colorOp[1]=(c>>18)&1u;g.colorOp[2]=(c>>19)&1u;g.colorOp[3]=(c>>20)&3u;
+ g.alpha[0]=(a>>13)&7u;g.alpha[1]=(a>>10)&7u;g.alpha[2]=(a>>7)&7u;g.alpha[3]=(a>>4)&7u;
+ g.alphaOp[0]=(a>>16)&3u;g.alphaOp[1]=(a>>18)&1u;g.alphaOp[2]=(a>>19)&1u;g.alphaOp[3]=(a>>20)&3u;
+ g.misc[0]=(c>>22)&3u;g.misc[1]=(a>>22)&3u;g.misc[2]=s.textured;g.misc[3]=s.rasterized;
+ std::memcpy(g.konst,s.konst.data(),sizeof(g.konst));
+ const auto& gen=material.texGens[s.texCoord];
+ const auto& m=gen.matrix;
+ const float row0[4]{m[0],m[1],m[2],gen.normal?1.f:0.f},row1[4]{m[3],m[4],m[5],float(gen.uvSet)};
+ std::memcpy(g.texGen[0],row0,sizeof(row0));std::memcpy(g.texGen[1],row1,sizeof(row1));
+ }
+ for(int i=0;i<4;++i) std::memcpy(out.registers[i],material.registers[i].data(),sizeof(out.registers[i]));
+ std::memcpy(out.materialColor,material.materialColor.data(),sizeof(out.materialColor));
+ out.info[0]=material.stageCount;out.info[1]=material.alphaCompare;
+ out.info[2]=material.colorControl;out.info[3]=material.alphaControl;
+ return out;
+}
+
+struct GpuModel {
+ wgpu::Buffer vertices;
+ std::vector textures;
+ std::vector uniforms;
+ std::vector materials;
+};
+inline std::array gpuModels;
+inline std::array gpuReady{};
+inline std::shared_ptr gpuArchive;
+inline uint64_t gpuRevision=0;
+inline wgpu::Texture whiteTexture;
+inline std::array samplers;
+inline wgpu::BindGroupLayout materialLayout,frameLayout;
+inline wgpu::PipelineLayout pipelineLayout;
+inline wgpu::ShaderModule shader;
+// One frame uniform per eye: both eyes may be encoded before one submit.
+inline std::array frameBuffers;
+inline std::array frameGroups;
+struct PipelineKey {
+ uint8_t cull=2,blendSrc=0,blendDst=0;
+ bool blend=false,subtract=false,depthWrite=true;
+ bool operator==(const PipelineKey&) const = default;
+};
+inline std::vector> pipelines;
+inline uint32_t pipelineSamples=0;
+inline bool pipelineReversed=false;
+inline wgpu::TextureFormat pipelineColor{},pipelineDepth{};
+
+inline void shutdown() {
+ gpuModels={};gpuReady={};gpuArchive.reset();gpuRevision=0;
+ pipelines.clear();samplers={};whiteTexture=nullptr;shader=nullptr;
+ frameBuffers={};frameGroups={};materialLayout=nullptr;frameLayout=nullptr;pipelineLayout=nullptr;
+ pipelineSamples=0;
+}
+
+inline void refresh_archive() {
+ const uint64_t revision=archiveRevision.load(std::memory_order_acquire);
+ if(revision==gpuRevision) return;
+ std::lock_guard lock(archiveMutex);
+ gpuArchive=archive;
+ gpuModels={};gpuReady={};
+ gpuRevision=revision;
+}
+
+// GX TEV, four stages at most (item materials use three). Konst selections are
+// resolved on the CPU; each stage samples its own binding with its own texgen.
+inline constexpr const char* tevShader=R"(
+ struct Stage { color: vec4u, colorOp: vec4u, alpha: vec4u, alphaOp: vec4u, misc: vec4u, konst: vec4f,
+ texGen0: vec4f, texGen1: vec4f };
+ struct Material { stages: array, registers: array, materialColor: vec4f, info: vec4u };
+ struct Frame { eye0: vec4f, eye1: vec4f, eye2: vec4f, seat0: vec4f, seat1: vec4f, seat2: vec4f,
+ projection: vec4f, depth: vec4f };
+ @group(0) @binding(0) var material: Material;
+ @group(0) @binding(1) var sampler0: sampler;
+ @group(0) @binding(2) var texture0: texture_2d;
+ @group(0) @binding(3) var sampler1: sampler;
+ @group(0) @binding(4) var texture1: texture_2d;
+ @group(0) @binding(5) var sampler2: sampler;
+ @group(0) @binding(6) var texture2: texture_2d;
+ @group(0) @binding(7) var sampler3: sampler;
+ @group(0) @binding(8) var texture3: texture_2d;
+ @group(1) @binding(0) var frame: Frame;
+ struct Out { @builtin(position) position: vec4f, @location(0) color: vec4f,
+ @location(1) uv01: vec4f, @location(2) uv23: vec4f };
+ fn unit(v: vec3f) -> vec3f { return v / max(length(v), 1e-4); }
+ // G3D texgen: a UV set or, for env maps, the view-space normal, then the SRT.
+ fn texCoord(s: u32, normal: vec3f, uv: vec4f) -> vec2f {
+ let g0 = material.stages[s].texGen0;
+ let g1 = material.stages[s].texGen1;
+ var base = select(uv.xy, uv.zw, g1.w > 0.5);
+ if (g0.w > 0.5) { base = vec2f(0.5 * normal.x + 0.5, -0.5 * normal.y + 0.5); }
+ return vec2f(dot(g0.xyz, vec3f(base, 1.0)), dot(g1.xyz, vec3f(base, 1.0)));
+ }
+ @vertex fn vs(@location(0) position: vec4f, @location(1) offset: vec4f, @location(2) normal: vec4f,
+ @location(3) color: vec4f, @location(4) uv: vec4f) -> Out {
+ let p4 = vec4f(position.xyz, 1.0);
+ let p = vec3f(dot(frame.eye0, p4), dot(frame.eye1, p4), dot(frame.eye2, p4)) + offset.xyz * frame.depth.z;
+ let billboard = position.w > 0.5;
+ let n = normal.xyz;
+ let eyeNormal = select(unit(vec3f(dot(frame.eye0.xyz, n), dot(frame.eye1.xyz, n), dot(frame.eye2.xyz, n))),
+ vec3f(0.0, 0.0, 1.0), billboard);
+ var lit = 1.0;
+ if (!billboard && (material.info.z & 2u) != 0u) {
+ let seatNormal = unit(vec3f(dot(frame.seat0.xyz, n), dot(frame.seat1.xyz, n), dot(frame.seat2.xyz, n)));
+ lit = 0.55 + 0.45 * abs(dot(seatNormal, vec3f(0.3, 0.8, 0.5)));
+ }
+ var o: Out;
+ let z = frame.depth.x * p.z + frame.depth.y;
+ o.position = vec4f(frame.projection.x * p.x + frame.projection.y * p.z,
+ frame.projection.z * p.y + frame.projection.w * p.z,
+ clamp(z, 0.0, max(-p.z, 0.0)), -p.z);
+ o.color = vec4f(select(material.materialColor.rgb, color.rgb, (material.info.z & 1u) != 0u) * lit,
+ select(material.materialColor.a, color.a, (material.info.w & 1u) != 0u));
+ o.uv01 = vec4f(texCoord(0u, eyeNormal, uv), texCoord(1u, eyeNormal, uv));
+ o.uv23 = vec4f(texCoord(2u, eyeNormal, uv), texCoord(3u, eyeNormal, uv));
+ return o;
+ }
+ fn colorIn(sel: u32, prev: vec4f, c0: vec4f, c1: vec4f, c2: vec4f, tex: vec4f, ras: vec4f, k: vec4f) -> vec3f {
+ switch sel {
+ case 0u: { return prev.rgb; } case 1u: { return vec3f(prev.a); }
+ case 2u: { return c0.rgb; } case 3u: { return vec3f(c0.a); }
+ case 4u: { return c1.rgb; } case 5u: { return vec3f(c1.a); }
+ case 6u: { return c2.rgb; } case 7u: { return vec3f(c2.a); }
+ case 8u: { return tex.rgb; } case 9u: { return vec3f(tex.a); }
+ case 10u: { return ras.rgb; } case 11u: { return vec3f(ras.a); }
+ case 12u: { return vec3f(1.0); } case 13u: { return vec3f(0.5); }
+ case 14u: { return k.rgb; }
+ default: { return vec3f(0.0); }
+ }
+ }
+ fn alphaIn(sel: u32, prev: vec4f, c0: vec4f, c1: vec4f, c2: vec4f, tex: vec4f, ras: vec4f, k: vec4f) -> f32 {
+ switch sel {
+ case 0u: { return prev.a; } case 1u: { return c0.a; } case 2u: { return c1.a; } case 3u: { return c2.a; }
+ case 4u: { return tex.a; } case 5u: { return ras.a; } case 6u: { return k.a; }
+ default: { return 0.0; }
+ }
+ }
+ fn tevBias(b: u32) -> f32 { if (b == 1u) { return 0.5; } if (b == 2u) { return -0.5; } return 0.0; }
+ fn tevScale(s: u32) -> f32 { if (s == 1u) { return 2.0; } if (s == 2u) { return 4.0; } if (s == 3u) { return 0.5; } return 1.0; }
+ fn alphaTest(f: u32, value: f32, reference: f32) -> bool {
+ switch f {
+ case 0u: { return false; } case 1u: { return value < reference; } case 2u: { return value == reference; }
+ case 3u: { return value <= reference; } case 4u: { return value > reference; }
+ case 5u: { return value != reference; } case 6u: { return value >= reference; }
+ default: { return true; }
+ }
+ }
+ @fragment fn fs(i: Out) -> @location(0) vec4f {
+ var samples = array(textureSample(texture0, sampler0, i.uv01.xy), textureSample(texture1, sampler1, i.uv01.zw),
+ textureSample(texture2, sampler2, i.uv23.xy), textureSample(texture3, sampler3, i.uv23.zw));
+ var prev = material.registers[0];
+ var c0 = material.registers[1];
+ var c1 = material.registers[2];
+ var c2 = material.registers[3];
+ var result = prev;
+ for (var s = 0u; s < min(material.info.x, 4u); s++) {
+ let st = material.stages[s];
+ let tex = select(vec4f(1.0), samples[s], st.misc.z != 0u);
+ let ras = select(vec4f(0.0), i.color, st.misc.w != 0u);
+ let ca = colorIn(st.color.x, prev, c0, c1, c2, tex, ras, st.konst);
+ let cb = colorIn(st.color.y, prev, c0, c1, c2, tex, ras, st.konst);
+ let cc = colorIn(st.color.z, prev, c0, c1, c2, tex, ras, st.konst);
+ let cd = colorIn(st.color.w, prev, c0, c1, c2, tex, ras, st.konst);
+ var color = (cd + select(1.0, -1.0, st.colorOp.y != 0u) * mix(ca, cb, cc) + tevBias(st.colorOp.x)) * tevScale(st.colorOp.w);
+ color = select(clamp(color, vec3f(-4.0), vec3f(4.0)), clamp(color, vec3f(0.0), vec3f(1.0)), st.colorOp.z != 0u);
+ let aa = alphaIn(st.alpha.x, prev, c0, c1, c2, tex, ras, st.konst);
+ let ab = alphaIn(st.alpha.y, prev, c0, c1, c2, tex, ras, st.konst);
+ let ac = alphaIn(st.alpha.z, prev, c0, c1, c2, tex, ras, st.konst);
+ let ad = alphaIn(st.alpha.w, prev, c0, c1, c2, tex, ras, st.konst);
+ var alpha = (ad + select(1.0, -1.0, st.alphaOp.y != 0u) * mix(aa, ab, ac) + tevBias(st.alphaOp.x)) * tevScale(st.alphaOp.w);
+ alpha = select(clamp(alpha, -4.0, 4.0), clamp(alpha, 0.0, 1.0), st.alphaOp.z != 0u);
+ switch st.misc.x {
+ case 1u: { c0 = vec4f(color, c0.a); } case 2u: { c1 = vec4f(color, c1.a); }
+ case 3u: { c2 = vec4f(color, c2.a); } default: { prev = vec4f(color, prev.a); }
+ }
+ switch st.misc.y {
+ case 1u: { c0.a = alpha; } case 2u: { c1.a = alpha; }
+ case 3u: { c2.a = alpha; } default: { prev.a = alpha; }
+ }
+ result = vec4f(color, alpha);
+ }
+ result = clamp(result, vec4f(0.0), vec4f(1.0));
+ let word = material.info.y;
+ let a8 = round(result.a * 255.0);
+ let pass0 = alphaTest((word >> 16u) & 7u, a8, f32(word & 255u));
+ let pass1 = alphaTest((word >> 19u) & 7u, a8, f32((word >> 8u) & 255u));
+ let logic = (word >> 22u) & 3u;
+ var passed = pass0 && pass1;
+ if (logic == 1u) { passed = pass0 || pass1; } else if (logic == 2u) { passed = pass0 != pass1; }
+ else if (logic == 3u) { passed = pass0 == pass1; }
+ if (!passed) { discard; }
+ return result;
+ }
+)";
+
+inline const wgpu::Sampler& sampler(uint8_t wrapS,uint8_t wrapT,bool mipmapped) {
+ auto& slot=samplers[(wrapS*3+wrapT)*2+mipmapped];
+ if(!slot) {
+ constexpr wgpu::AddressMode modes[3]{wgpu::AddressMode::ClampToEdge,wgpu::AddressMode::Repeat,
+ wgpu::AddressMode::MirrorRepeat};
+ const wgpu::SamplerDescriptor desc{.label="Cockpit item sampler",
+ .addressModeU=modes[wrapS],.addressModeV=modes[wrapT],
+ .magFilter=wgpu::FilterMode::Linear,.minFilter=wgpu::FilterMode::Linear,
+ .mipmapFilter=mipmapped?wgpu::MipmapFilterMode::Linear:wgpu::MipmapFilterMode::Nearest};
+ slot=webgpu::g_device.CreateSampler(&desc);
+ }
+ return slot;
+}
+
+inline void prepare_layout() {
+ using namespace webgpu;
+ if(materialLayout) return;
+ std::array entries{};
+ entries[0]={.binding=0,.visibility=wgpu::ShaderStage::Vertex|wgpu::ShaderStage::Fragment,
+ .buffer=wgpu::BufferBindingLayout{.type=wgpu::BufferBindingType::Uniform,.minBindingSize=sizeof(GpuMaterial)}};
+ for(uint32_t i=0;i<4;++i) {
+ entries[1+i*2]={.binding=1+i*2,.visibility=wgpu::ShaderStage::Fragment,
+ .sampler=wgpu::SamplerBindingLayout{.type=wgpu::SamplerBindingType::Filtering}};
+ entries[2+i*2]={.binding=2+i*2,.visibility=wgpu::ShaderStage::Fragment,
+ .texture=wgpu::TextureBindingLayout{.sampleType=wgpu::TextureSampleType::Float,
+ .viewDimension=wgpu::TextureViewDimension::e2D}};
+ }
+ const wgpu::BindGroupLayoutDescriptor materialDesc{.entryCount=entries.size(),.entries=entries.data()};
+ materialLayout=g_device.CreateBindGroupLayout(&materialDesc);
+ const wgpu::BindGroupLayoutEntry frameEntry{.binding=0,.visibility=wgpu::ShaderStage::Vertex,
+ .buffer=wgpu::BufferBindingLayout{.type=wgpu::BufferBindingType::Uniform,.minBindingSize=sizeof(GpuFrame)}};
+ const wgpu::BindGroupLayoutDescriptor frameDesc{.entryCount=1,.entries=&frameEntry};
+ frameLayout=g_device.CreateBindGroupLayout(&frameDesc);
+ const std::array layouts{materialLayout,frameLayout};
+ const wgpu::PipelineLayoutDescriptor layoutDesc{.bindGroupLayoutCount=layouts.size(),.bindGroupLayouts=layouts.data()};
+ pipelineLayout=g_device.CreatePipelineLayout(&layoutDesc);
+ wgpu::ShaderSourceWGSL source{};
+ source.code=tevShader;
+ wgpu::ShaderModuleDescriptor md{};md.nextInChain=&source;md.label="Cockpit item TEV";
+ shader=g_device.CreateShaderModule(&md);
+ for(uint32_t eye=0;eye<2;++eye) {
+ const wgpu::BufferDescriptor bufferDesc{.label="Cockpit item frame",
+ .usage=wgpu::BufferUsage::Uniform|wgpu::BufferUsage::CopyDst,.size=sizeof(GpuFrame)};
+ frameBuffers[eye]=g_device.CreateBuffer(&bufferDesc);
+ const wgpu::BindGroupEntry entry{.binding=0,.buffer=frameBuffers[eye],.size=sizeof(GpuFrame)};
+ const wgpu::BindGroupDescriptor group{.layout=frameLayout,.entryCount=1,.entries=&entry};
+ frameGroups[eye]=g_device.CreateBindGroup(&group);
+ }
+ const wgpu::TextureDescriptor desc{.label="Cockpit item white",
+ .usage=wgpu::TextureUsage::TextureBinding|wgpu::TextureUsage::CopyDst,
+ .dimension=wgpu::TextureDimension::e2D,.size={1,1,1},.format=wgpu::TextureFormat::RGBA8Unorm,
+ .mipLevelCount=1,.sampleCount=1};
+ whiteTexture=g_device.CreateTexture(&desc);
+ const uint8_t white[4]{255,255,255,255};
+ const wgpu::TexelCopyTextureInfo destination{.texture=whiteTexture};
+ const wgpu::TexelCopyBufferLayout layout{.bytesPerRow=4,.rowsPerImage=1};
+ const wgpu::Extent3D extent{1,1,1};
+ g_queue.WriteTexture(&destination,white,4,&layout,&extent);
+}
+
+inline void prepare_model(size_t index) {
+ using namespace webgpu;
+ const auto& model=gpuArchive->models[index];
+ auto& gpu=gpuModels[index];
+ std::vector vertices;
+ for(const auto& part:model.parts) for(const auto& v:part.vertices) {
+ const data::V3 at=part.billboard?part.origin:v.position,offset=part.billboard?v.position:data::V3{};
+ vertices.push_back({{at.x,at.y,at.z,part.billboard?1.f:0.f},{offset.x,offset.y,offset.z,0},
+ {v.normal.x,v.normal.y,v.normal.z,0},{v.color[0],v.color[1],v.color[2],v.color[3]},
+ {v.uv[0].x,v.uv[0].y,v.uv[1].x,v.uv[1].y}});
+ }
+ const wgpu::BufferDescriptor vertexDesc{.label="Cockpit item vertices",
+ .usage=wgpu::BufferUsage::Vertex|wgpu::BufferUsage::CopyDst,.size=vertices.size()*sizeof(GpuVertex)};
+ gpu.vertices=g_device.CreateBuffer(&vertexDesc);
+ g_queue.WriteBuffer(gpu.vertices,0,vertices.data(),vertices.size()*sizeof(GpuVertex));
+ for(const auto& texture:model.textures) {
+ const bool usable=!texture.rgba.empty() && texture.rgba.size()>=mip_bytes(texture,texture.mips);
+ if(!usable) { gpu.textures.push_back(nullptr);continue; }
+ const wgpu::TextureDescriptor desc{.label="Cockpit item texture",
+ .usage=wgpu::TextureUsage::TextureBinding|wgpu::TextureUsage::CopyDst,
+ .dimension=wgpu::TextureDimension::e2D,.size={texture.width,texture.height,1},
+ .format=wgpu::TextureFormat::RGBA8Unorm,.mipLevelCount=texture.mips,.sampleCount=1};
+ auto gpuTexture=g_device.CreateTexture(&desc);
+ size_t offset=0;
+ for(uint32_t level=0;level>level,1),h=std::max(texture.height>>level,1);
+ const wgpu::TexelCopyTextureInfo destination{.texture=gpuTexture,.mipLevel=level};
+ const wgpu::TexelCopyBufferLayout layout{.bytesPerRow=w*4,.rowsPerImage=h};
+ const wgpu::Extent3D extent{w,h,1};
+ g_queue.WriteTexture(&destination,texture.rgba.data()+offset,size_t(w)*h*4,&layout,&extent);
+ offset+=size_t(w)*h*4;
+ }
+ gpu.textures.push_back(std::move(gpuTexture));
+ }
+ for(const auto& material:model.materials) {
+ const auto uniform=gpu_material(material);
+ const wgpu::BufferDescriptor bufferDesc{.label="Cockpit item material",
+ .usage=wgpu::BufferUsage::Uniform|wgpu::BufferUsage::CopyDst,.size=sizeof(GpuMaterial)};
+ auto buffer=g_device.CreateBuffer(&bufferDesc);
+ g_queue.WriteBuffer(buffer,0,&uniform,sizeof(uniform));
+ std::array entries{};
+ entries[0]={.binding=0,.buffer=buffer,.size=sizeof(GpuMaterial)};
+ for(uint32_t i=0;i<4;++i) {
+ const auto& stage=material.stages[i];
+ const data::Map* map=itexture]?&gpu.textures[map->texture]:nullptr;
+ const bool mipmapped=texture && model.textures[map->texture].mips>1;
+ entries[1+i*2]={.binding=1+i*2,.sampler=texture?sampler(map->wrapS,map->wrapT,mipmapped):sampler(1,1,false)};
+ entries[2+i*2]={.binding=2+i*2,.textureView=(texture?*texture:whiteTexture).CreateView()};
+ }
+ const wgpu::BindGroupDescriptor group{.layout=materialLayout,.entryCount=entries.size(),.entries=entries.data()};
+ gpu.materials.push_back(g_device.CreateBindGroup(&group));
+ gpu.uniforms.push_back(std::move(buffer));
+ }
+ gpuReady[index]=true;
+}
+
+inline wgpu::BlendFactor blend_factor(uint8_t factor,bool source) {
+ switch(factor) {
+ case 0: return wgpu::BlendFactor::Zero;
+ case 1: return wgpu::BlendFactor::One;
+ case 2: return source?wgpu::BlendFactor::Dst:wgpu::BlendFactor::Src;
+ case 3: return source?wgpu::BlendFactor::OneMinusDst:wgpu::BlendFactor::OneMinusSrc;
+ case 4: return wgpu::BlendFactor::SrcAlpha;
+ case 5: return wgpu::BlendFactor::OneMinusSrcAlpha;
+ case 6: return wgpu::BlendFactor::One; // The eye target's alpha is not the EFB's.
+ default: return wgpu::BlendFactor::Zero;
+ }
+}
+inline const wgpu::RenderPipeline& pipeline(const PipelineKey& key,const StereoReplayFrame& frame,uint32_t eye,bool reversed) {
+ using namespace webgpu;
+ const auto& target=frame.eyes[eye].target;
+ const auto format=g_graphicsConfig.surfaceConfiguration.format;
+ if(pipelineSamples!=target.msaaSamples || pipelineColor!=format ||
+ pipelineDepth!=target.depthFormat || pipelineReversed!=reversed) {
+ pipelines.clear();
+ pipelineSamples=target.msaaSamples;pipelineColor=format;pipelineDepth=target.depthFormat;pipelineReversed=reversed;
+ }
+ for(const auto& [cached,value]:pipelines) if(cached==key) return value;
+ const wgpu::VertexAttribute attrs[]{
+ {.format=wgpu::VertexFormat::Float32x4,.offset=0,.shaderLocation=0},
+ {.format=wgpu::VertexFormat::Float32x4,.offset=16,.shaderLocation=1},
+ {.format=wgpu::VertexFormat::Float32x4,.offset=32,.shaderLocation=2},
+ {.format=wgpu::VertexFormat::Float32x4,.offset=48,.shaderLocation=3},
+ {.format=wgpu::VertexFormat::Float32x4,.offset=64,.shaderLocation=4},
+ };
+ const wgpu::VertexBufferLayout vertices{.arrayStride=sizeof(GpuVertex),.attributeCount=5,.attributes=attrs};
+ const wgpu::BlendState blend{
+ .color=key.subtract?wgpu::BlendComponent{.operation=wgpu::BlendOperation::ReverseSubtract,
+ .srcFactor=wgpu::BlendFactor::One,.dstFactor=wgpu::BlendFactor::One}
+ :wgpu::BlendComponent{.operation=wgpu::BlendOperation::Add,
+ .srcFactor=blend_factor(key.blendSrc,true),
+ .dstFactor=blend_factor(key.blendDst,false)},
+ .alpha={.operation=wgpu::BlendOperation::Add,.srcFactor=wgpu::BlendFactor::One,
+ .dstFactor=wgpu::BlendFactor::OneMinusSrcAlpha},
+ };
+ const wgpu::ColorTargetState color{.format=format,.blend=key.blend?&blend:nullptr};
+ const wgpu::FragmentState fragment{.module=shader,.entryPoint="fs",.targetCount=1,.targets=&color};
+ const bool stencil=target.depthFormat==wgpu::TextureFormat::Depth24PlusStencil8;
+ const wgpu::StencilFaceState mark{.compare=wgpu::CompareFunction::Always,
+ .passOp=stencil?wgpu::StencilOperation::Replace:wgpu::StencilOperation::Keep};
+ const wgpu::DepthStencilState depth{.format=target.depthFormat,.depthWriteEnabled=key.depthWrite,
+ .depthCompare=reversed?wgpu::CompareFunction::GreaterEqual:wgpu::CompareFunction::LessEqual,
+ .stencilFront=mark,.stencilBack=mark,.stencilReadMask=1,.stencilWriteMask=stencil?1u:0u};
+ wgpu::RenderPipelineDescriptor desc{};desc.label="Cockpit item";desc.layout=pipelineLayout;
+ desc.vertex={.module=shader,.entryPoint="vs",.bufferCount=1,.buffers=&vertices};
+ desc.fragment=&fragment;desc.depthStencil=&depth;desc.multisample.count=target.msaaSamples;
+ desc.primitive.topology=wgpu::PrimitiveTopology::TriangleList;
+ // Same winding as Aurora's GX pipelines: GX front faces are clockwise.
+ desc.primitive.frontFace=wgpu::FrontFace::CW;
+ desc.primitive.cullMode=key.cull==1?wgpu::CullMode::Front:key.cull==2?wgpu::CullMode::Back:wgpu::CullMode::None;
+ pipelines.emplace_back(key,g_device.CreateRenderPipeline(&desc));
+ return pipelines.back().second;
+}
+
+inline bool finite_matrix(const float* m) {
+ for(int i=0;i<12;++i) {uint32_t bits;std::memcpy(&bits,m+i,4);if((bits&0x7f800000u)==0x7f800000u) return false;}
+ return true;
+}
+inline void compose(const float* a,const float* b,float* out) {
+ for(int r=0;r<3;++r) for(int c=0;c<4;++c) {
+ out[r*4+c]=c==3?a[r*4+3]:0;
+ for(int k=0;k<3;++k) out[r*4+c]+=a[r*4+k]*b[k*4+c];
+ }
+}
+
+// The held item's frame in the seated frame (+X right, +Y up, -Z forward). It
+// stays upright whatever the hand's roll and pitch, sits just above the palm and
+// turns only with the hand's heading, facing back along it: the item's front
+// (+Z) faces the player while the fingers point ahead. The fingers run along
+// grip -Y and along the palm joint's -Z.
+inline bool seat_from_item(const AuroraCockpitHand& hand,std::array& out) {
+ const float* pose=hand.jointsValid?hand.seatFromJoint[0]:hand.seatFromGrip;
+ if(!finite_matrix(pose)) return false;
+ const int fingers=hand.jointsValid?2:1;
+ // The heading only. Within ~9 degrees of pointing straight up or down it
+ // fades to straight ahead instead of spinning the item.
+ const float x=-pose[fingers],z=-pose[8+fingers];
+ const float length=std::sqrt(x*x+z*z),weight=std::clamp(length/0.15f,0.0f,1.0f);
+ float headingX=weight*x/std::max(length,1e-6f),headingZ=weight*z/std::max(length,1e-6f)-(1-weight);
+ float heading=std::sqrt(headingX*headingX+headingZ*headingZ);
+ if(!(heading>1e-4f)) { headingX=0;headingZ=-1;heading=1; }
+ const float frontX=-headingX/heading,frontZ=-headingZ/heading;
+ constexpr float lift=0.05f;
+ out={frontZ,0,frontX,pose[3], 0,1,0,pose[7]+lift, -frontX,0,frontZ,pose[11]};
+ return true;
+}
+// Scales a model to 14 cm across its largest side and stands it on the item
+// frame's origin, centred.
+inline std::array item_from_model(const data::Model& model) {
+ const float span=std::max({model.maximum.x-model.minimum.x,model.maximum.y-model.minimum.y,
+ model.maximum.z-model.minimum.z});
+ if(!(span>0.001f) || !std::isfinite(span)) return {};
+ const float k=0.14f/span;
+ const float cx=(model.minimum.x+model.maximum.x)*0.5f,cz=(model.minimum.z+model.maximum.z)*0.5f;
+ return {k,0,0,-k*cx, 0,k,0,-k*model.minimum.y, 0,0,k,-k*cz};
+}
+
+inline void render(const wgpu::RenderPassEncoder& pass,const StereoReplayFrame& frame,uint32_t eye,
+ float sceneZ,float sceneConstant) {
+ if(!frame.cockpit.active || !(frame.cockpit.unitsPerMeter>0) || eye>1) return;
+ refresh_archive();
+ if(!gpuArchive) return;
+ prepare_layout();
+ // Upload on registration, before the first roulette settles, so receiving an
+ // item does not pause the frame on texture uploads.
+ for(size_t i=0;imodels.size();++i)
+ if(!gpuReady[i] && gpuArchive->models[i].valid()) prepare_model(i);
+ const auto& item=frame.cockpitItem;
+ if(!item.valid || item.hand>1 || item.count==0 || item.count>3) return;
+ const int index=data::model_index(item.id);
+ if(index<0 || !gpuArchive->models[index].valid()) return;
+ const auto& hand=frame.cockpit.hands[item.hand];
+ std::array seatFromItem;
+ if(!hand.tracked || !seat_from_item(hand,seatFromItem)) return;
+ const auto& model=gpuArchive->models[index];
+ const auto itemFromModel=item_from_model(model);
+ if(!(itemFromModel[0]>0)) return;
+ GpuFrame uniform{};
+ compose(seatFromItem.data(),itemFromModel.data(),uniform.seatFromModel);
+ compose(frame.cockpit.eyeFromSeat[eye],uniform.seatFromModel,uniform.eyeFromModel);
+ const auto& projection=frame.eyes[eye].projection;
+ const float projectionRow[4]{projection.m0[0],projection.m0[2],projection.m1[1],projection.m1[2]};
+ const float depth[4]{sceneZ,sceneConstant/std::max(frame.cockpit.unitsPerMeter,0.001f),itemFromModel[0],0};
+ std::memcpy(uniform.projection,projectionRow,sizeof(projectionRow));
+ std::memcpy(uniform.depth,depth,sizeof(depth));
+ webgpu::g_queue.WriteBuffer(frameBuffers[eye],0,&uniform,sizeof(uniform));
+ const auto& gpu=gpuModels[index];
+ pass.SetVertexBuffer(0,gpu.vertices);
+ pass.SetBindGroup(1,frameGroups[eye],0,nullptr);
+ const bool reversed=sceneConstant>0;
+ uint32_t start=0;
+ for(const auto& part:model.parts) {
+ const auto count=static_cast(part.vertices.size());
+ const auto& material=model.materials[part.material];
+ if(material.cull!=3) {
+ const PipelineKey key{material.cull,material.blendSrc,material.blendDst,material.blend,material.subtract,
+ material.depthWrite};
+ pass.SetPipeline(pipeline(key,frame,eye,reversed));
+ pass.SetBindGroup(0,gpu.materials[part.material],0,nullptr);
+ pass.Draw(count,1,start,0);
+ }
+ start+=count;
+ }
+}
+
+} // namespace aurora::gfx::cockpit_item
diff --git a/aurora-main/lib/gfx/cockpit_item_data.hpp b/aurora-main/lib/gfx/cockpit_item_data.hpp
new file mode 100644
index 0000000..3058521
--- /dev/null
+++ b/aurora-main/lib/gfx/cockpit_item_data.hpp
@@ -0,0 +1,577 @@
+// SPDX-License-Identifier: GPL-3.0-or-later
+#pragma once
+
+// Small, bounded reader for MKW's item BRRES models: bind-pose geometry plus
+// each material's texture layers, texgens and TEV stages. All input is copied
+// from the user's mapped Common.szs. No game data is shipped.
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+
+namespace aurora::gfx::cockpit_item::data {
+
+struct Reader {
+ const uint8_t* bytes = nullptr;
+ size_t size = 0;
+ bool has(size_t at, size_t count) const { return at <= size && count <= size - at; }
+ uint8_t u8(size_t at) const { return has(at,1) ? bytes[at] : 0; }
+ uint16_t u16(size_t at) const { return has(at,2) ? (uint16_t(bytes[at])<<8)|bytes[at+1] : 0; }
+ uint32_t u32(size_t at) const {
+ return has(at,4) ? (uint32_t(bytes[at])<<24)|(uint32_t(bytes[at+1])<<16)|
+ (uint32_t(bytes[at+2])<<8)|bytes[at+3] : 0;
+ }
+ float f32(size_t at) const { uint32_t bits=u32(at); float out; std::memcpy(&out,&bits,4); return out; }
+ // A section-relative offset: returns 0 (never valid here) when it leaves the file.
+ size_t rel(size_t base,size_t at) const {
+ const int64_t target=int64_t(base)+int32_t(u32(at));
+ return target>0 && size_t(target)=size) return {};
+ size_t end=at;
+ while(end(bytes+at),end-at) : std::string{};
+ }
+};
+
+struct Entry { std::string name; size_t at; };
+inline std::vector dict(Reader r,size_t at) {
+ if(!r.has(at,8)) return {};
+ const uint32_t count=r.u32(at+4);
+ if(count>4096 || !r.has(at+8,size_t(count+1)*16)) return {};
+ std::vector out;
+ out.reserve(count);
+ for(uint32_t i=1;i<=count;++i) {
+ const size_t e=at+8+size_t(i)*16;
+ const size_t target=at+r.u32(e+12);
+ const auto name=r.str(at+r.u32(e+8));
+ if(name.empty() || target>=r.size) return {};
+ out.push_back({name,target});
+ }
+ return out;
+}
+inline size_t find(const std::vector& entries,const std::string& name) {
+ for(const auto& e:entries) if(e.name==name) return e.at;
+ return 0;
+}
+
+inline std::vector yaz0(Reader input) {
+ if(input.size>32u*1024u*1024u || !input.has(0,16)) return {};
+ if(input.u32(0)!=0x59617a30u) return std::vector(input.bytes,input.bytes+input.size);
+ const size_t length=input.u32(4);
+ if(length==0 || length>32u*1024u*1024u) return {};
+ std::vector out;
+ out.reserve(length);
+ size_t at=16;
+ while(out.size()=0 && out.size()>4;
+ if(count) count+=2;
+ else { if(!input.has(at,1)) return {}; count=size_t(input.u8(at++))+18; }
+ const size_t distance=((size_t(a&15)<<8)|b)+1;
+ if(distance>out.size() || count>length-out.size()) return {};
+ for(size_t j=0;j;
+struct Matrix {
+ std::array v{1,0,0,0,0,1,0,0,0,0,1,0};
+};
+inline V3 point(const Matrix& m,V3 p) {
+ const auto& a=m.v;
+ return {a[0]*p.x+a[1]*p.y+a[2]*p.z+a[3],a[4]*p.x+a[5]*p.y+a[6]*p.z+a[7],
+ a[8]*p.x+a[9]*p.y+a[10]*p.z+a[11]};
+}
+inline V3 direction(const Matrix& m,V3 p) {
+ const auto& a=m.v;
+ return {a[0]*p.x+a[1]*p.y+a[2]*p.z,a[4]*p.x+a[5]*p.y+a[6]*p.z,
+ a[8]*p.x+a[9]*p.y+a[10]*p.z};
+}
+
+inline float component(Reader r,size_t at,uint32_t type,uint8_t shift) {
+ if(type==4) return r.f32(at);
+ const float scale=std::ldexp(1.0f,-int(shift));
+ if(type==0) return r.u8(at)*scale;
+ if(type==1) return int8_t(r.u8(at))*scale;
+ if(type==2) return r.u16(at)*scale;
+ if(type==3) return int16_t(r.u16(at))*scale;
+ return 0;
+}
+struct Array {
+ uint32_t id=0;
+ std::vector values;
+};
+enum class ArrayKind { Position, Normal, UV };
+inline std::vector arrays(Reader r,size_t model,uint32_t dictionary_offset,ArrayKind kind) {
+ std::vector result;
+ if(!dictionary_offset) return result;
+ for(const auto& e:dict(r,model+dictionary_offset)) {
+ const size_t h=e.at;
+ if(!r.has(h,0x20)) return {};
+ const uint32_t type=r.u32(h+0x18), comps=r.u32(h+0x14);
+ const uint8_t shift=r.u8(h+0x1c), stride=r.u8(h+0x1d);
+ const uint16_t count=r.u16(h+0x1e);
+ const size_t data=h+r.u32(h+8);
+ const size_t elem=type==4?4:(type==2||type==3?2:1);
+ const size_t n=kind==ArrayKind::UV?(comps?2:1):
+ kind==ArrayKind::Normal?(comps?9:3):(comps?3:2);
+ if(type>4 || stride1?component(r,p+elem,type,shift):0,
+ n>2?component(r,p+2*elem,type,shift):0});
+ }
+ result.push_back(std::move(a));
+ }
+ return result;
+}
+template inline const T* array_id(const std::vector& entries,uint16_t id) {
+ for(const auto& a:entries) if(a.id==id) return &a;
+ return nullptr;
+}
+struct ColorArray {
+ uint32_t id=0;
+ std::vector values;
+};
+// GX colour array formats: RGB565, RGB8, RGBX8, RGBA4, RGBA6, RGBA8.
+inline std::vector color_arrays(Reader r,size_t model,uint32_t dictionary_offset) {
+ std::vector result;
+ if(!dictionary_offset) return result;
+ constexpr uint8_t sizes[6]{2,3,4,2,3,4};
+ for(const auto& e:dict(r,model+dictionary_offset)) {
+ const size_t h=e.at;
+ if(!r.has(h,0x20)) return {};
+ const uint32_t format=r.u32(h+0x18);
+ const uint8_t stride=r.u8(h+0x1c);
+ const uint16_t count=r.u16(h+0x1e);
+ const size_t data=h+r.u32(h+8);
+ if(format>5 || stride>11)&31)/31.f,((v16>>5)&63)/63.f,(v16&31)/31.f,1}; break;
+ case 1: case 2: c={r.u8(p)/255.f,r.u8(p+1)/255.f,r.u8(p+2)/255.f,1}; break;
+ case 3: c={(v16>>12)/15.f,((v16>>8)&15)/15.f,((v16>>4)&15)/15.f,(v16&15)/15.f}; break;
+ case 4: c={(v24>>18)/63.f,((v24>>12)&63)/63.f,((v24>>6)&63)/63.f,(v24&63)/63.f}; break;
+ default: c={r.u8(p)/255.f,r.u8(p+1)/255.f,r.u8(p+2)/255.f,r.u8(p+3)/255.f}; break;
+ }
+ a.values.push_back(c);
+ }
+ result.push_back(std::move(a));
+ }
+ return result;
+}
+
+// G3D texture SRT in Maya mode (every item material uses it), as a 2x3 matrix
+// applied to (s,t,1). Other modes fall back to a plain scale-rotate-translate.
+inline std::array texture_srt(float sx,float sy,float degrees,float tx,float ty,uint32_t mode) {
+ const float r=degrees*0.017453292519943295f,c=std::cos(r),s=std::sin(r);
+ if(mode!=0) return {sx*c,-sy*s,tx,sx*s,sy*c,ty};
+ return {sx*c,sy*-s,sx*(-0.5f*c-(0.5f*s-0.5f)-tx),
+ sx*s,sy*c,sy*(-0.5f*c+(0.5f*s-0.5f)+ty)+1.0f};
+}
+
+struct Vertex { V3 position;V3 normal;Color color{1,1,1,1};std::array uv{}; };
+struct Texture {
+ std::string name;
+ uint16_t width=0,height=0;
+ uint32_t format=0,mips=1;
+ std::vector bytes;
+ std::vector rgba; // every mip level, level 0 first
+};
+struct TexGen {
+ bool normal=false; // env map from the view-space normal; else a UV set
+ uint8_t uvSet=0;
+ std::array matrix{1,0,0,0,1,0};
+};
+struct Stage {
+ uint8_t texMap=0,texCoord=0;
+ bool textured=false,rasterized=true;
+ uint32_t color=0x8fff0,alpha=0; // BP 0xC0/0xC1 combiner words
+ Color konst{1,1,1,1}; // the stage's resolved KSEL constant
+};
+struct Map { int texture=-1;uint8_t wrapS=1,wrapT=1; };
+struct Material {
+ uint8_t cull=2; // GX: 0 none, 1 front, 2 back, 3 all
+ bool blend=false,subtract=false,depthWrite=true;
+ uint8_t blendSrc=4,blendDst=5;
+ uint32_t alphaCompare=0x3f0000; // BP 0xF3
+ uint32_t colorControl=0x700,alphaControl=0x700;
+ Color materialColor{1,1,1,1};
+ uint8_t stageCount=0;
+ std::array stages{};
+ std::array registers{}; // PREV, C0, C1, C2
+ std::array texGens{};
+ std::array