Added First Person VR item management

- Implemented DVDReadVrAsset function to read mapped disc paths for VR assets.
- Created HeldItem structure and ReadHeldItem function for managing held items in the game.
- Developed unit tests for ReadHeldItem to ensure correct functionality and edge case handling.
- Added cockpit item data tests to validate model indexing and parsing of archives.
This commit is contained in:
iChris4 committed 2026-09-29 23:55:52 +02:00
1 parent 75f075b234
commit db30945f4c
26 files changed
+1598 -19

No files matched your search

+14
View File
@@ -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
@@ -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.
@@ -535,6 +535,11 @@
<string name="vr_seat_custom">Custom</string>
<string name="vr_hand_steering">Hand steering</string>
<string name="vr_hand_steering_helper">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.</string>
<string name="vr_cockpit_item_hand">Item in cockpit hand</string>
<string name="vr_cockpit_item_hand_helper">Show Player 1\'s settled inventory item above the selected palm. Triple items show the remaining count. Using or losing the item hides it.</string>
<string name="vr_item_left">Left</string>
<string name="vr_item_right">Right</string>
<string name="vr_item_off">Off</string>
<string name="vr_hand_tracking">Tracked hands</string>
<string name="vr_hand_tracking_helper">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.</string>
<string name="vr_lean_back">Lean back angle</string>
+15
View File
@@ -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.
+10
View File
@@ -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;
}
+39 -3
View File
@@ -7,6 +7,7 @@
// and hand steering".
#pragma once
#include "common.hpp"
#include "cockpit_item.hpp"
#include "../webgpu/gpu.hpp"
#include <array>
#include <atomic>
@@ -251,12 +252,44 @@ inline void build_geometry(const AuroraCockpit& cockpit, std::vector<Vertex>& ve
else glove(vertices,hand,side);
}
}
inline void append_item_badge(const AuroraCockpit& cockpit,const AuroraCockpitItem& item,
std::vector<Vertex>& 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<<bit)) tube(vertices,add(base,{x0,y0,0.005f}),add(base,{x1,y1,0.005f}),0.0016f,white,5);
};
segment(0,-0.007f, 0.010f, 0.007f, 0.010f);
segment(1, 0.008f, 0.009f, 0.008f, 0.001f);
segment(2, 0.008f,-0.001f, 0.008f,-0.009f);
segment(3,-0.007f,-0.010f, 0.007f,-0.010f);
segment(4,-0.008f,-0.009f,-0.008f,-0.001f);
segment(5,-0.008f, 0.001f,-0.008f, 0.009f);
segment(6,-0.007f, 0.0f, 0.007f, 0.0f);
for(size_t i=first;i<vertices.size();++i) vertices[i].position=point(frame.data(),vertices[i].position);
}
inline std::vector<Vertex> geometry(const AuroraCockpit& cockpit) {
std::vector<Vertex> result;build_geometry(cockpit,result);return result;
}
inline std::atomic<uint64_t> meshRevision{1};
inline std::vector<Vertex> 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<wgpu::Buffer,2> vertexBuffers;
inline std::array<uint64_t,2> 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();
}
+528
View File
@@ -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 <array>
#include <atomic>
#include <cstring>
#include <memory>
#include <mutex>
namespace aurora::gfx::cockpit_item {
inline std::mutex archiveMutex;
inline std::shared_ptr<const data::Archive> archive;
inline std::atomic<uint64_t> archiveRevision{0};
inline size_t mip_bytes(const data::Texture& texture,uint32_t mips) {
size_t total=0;
for(uint32_t level=0;level<mips;++level)
total+=size_t(std::max(texture.width>>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::Archive>(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<uint8_t>(texture.bytes));
const size_t length=mip_bytes(texture,mips);
if(converted.format!=wgpu::TextureFormat::RGBA8Unorm || converted.data.size()<length) continue;
texture.rgba.assign(converted.data.data(),converted.data.data()+length);
texture.mips=mips;
break;
}
}
std::lock_guard lock(archiveMutex);
if(archive) return;
archive=parsed->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<material.stageCount;++i) {
const auto& s=material.stages[i];
auto& g=out.stages[i];
const uint32_t c=s.color,a=s.alpha;
g.color[0]=(c>>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<wgpu::Texture> textures;
std::vector<wgpu::Buffer> uniforms;
std::vector<wgpu::BindGroup> materials;
};
inline std::array<GpuModel,15> gpuModels;
inline std::array<bool,15> gpuReady{};
inline std::shared_ptr<const data::Archive> gpuArchive;
inline uint64_t gpuRevision=0;
inline wgpu::Texture whiteTexture;
inline std::array<wgpu::Sampler,18> 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<wgpu::Buffer,2> frameBuffers;
inline std::array<wgpu::BindGroup,2> 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<std::pair<PipelineKey,wgpu::RenderPipeline>> 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<Stage, 4>, registers: array<vec4f, 4>, 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<uniform> material: Material;
@group(0) @binding(1) var sampler0: sampler;
@group(0) @binding(2) var texture0: texture_2d<f32>;
@group(0) @binding(3) var sampler1: sampler;
@group(0) @binding(4) var texture1: texture_2d<f32>;
@group(0) @binding(5) var sampler2: sampler;
@group(0) @binding(6) var texture2: texture_2d<f32>;
@group(0) @binding(7) var sampler3: sampler;
@group(0) @binding(8) var texture3: texture_2d<f32>;
@group(1) @binding(0) var<uniform> 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<vec4f, 4>(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<wgpu::BindGroupLayoutEntry,9> 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<GpuVertex> 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<texture.mips;++level) {
const uint32_t w=std::max(texture.width>>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<wgpu::BindGroupEntry,9> 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=i<material.stageCount && stage.textured?&material.maps[stage.texMap]:nullptr;
const wgpu::Texture* texture=map && gpu.textures[map->texture]?&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<float,12>& 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<float,12> 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;i<gpuArchive->models.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<float,12> 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<uint32_t>(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
+577
View File
@@ -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 <algorithm>
#include <array>
#include <cmath>
#include <cstddef>
#include <cstdint>
#include <cstring>
#include <limits>
#include <string>
#include <vector>
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 ? size_t(target) : 0;
}
std::string str(size_t at) const {
if(at>=size) return {};
size_t end=at;
while(end<size && end-at<128 && bytes[end]) ++end;
return end<size && end-at<128 ? std::string(reinterpret_cast<const char*>(bytes+at),end-at) : std::string{};
}
};
struct Entry { std::string name; size_t at; };
inline std::vector<Entry> 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<Entry> 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<Entry>& entries,const std::string& name) {
for(const auto& e:entries) if(e.name==name) return e.at;
return 0;
}
inline std::vector<uint8_t> yaz0(Reader input) {
if(input.size>32u*1024u*1024u || !input.has(0,16)) return {};
if(input.u32(0)!=0x59617a30u) return std::vector<uint8_t>(input.bytes,input.bytes+input.size);
const size_t length=input.u32(4);
if(length==0 || length>32u*1024u*1024u) return {};
std::vector<uint8_t> out;
out.reserve(length);
size_t at=16;
while(out.size()<length) {
if(!input.has(at,1)) return {};
const uint8_t control=input.u8(at++);
for(int bit=7;bit>=0 && out.size()<length;--bit) {
if(control & (1u<<bit)) {
if(!input.has(at,1)) return {};
out.push_back(input.u8(at++));
} else {
if(!input.has(at,2)) return {};
const uint8_t a=input.u8(at++), b=input.u8(at++);
size_t count=a>>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<count;++j) out.push_back(out[out.size()-distance]);
}
}
}
return out;
}
struct V3 { float x=0,y=0,z=0; };
struct V2 { float x=0,y=0; };
using Color = std::array<float,4>;
struct Matrix {
std::array<float,12> 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<V3> values;
};
enum class ArrayKind { Position, Normal, UV };
inline std::vector<Array> arrays(Reader r,size_t model,uint32_t dictionary_offset,ArrayKind kind) {
std::vector<Array> 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 || stride<n*elem || !r.has(data,size_t(count)*stride)) return {};
Array a; a.id=r.u32(h+0x10);a.values.reserve(count);
for(uint32_t i=0;i<count;++i) {
const size_t p=data+size_t(i)*stride;
a.values.push_back({component(r,p,type,shift),n>1?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 <typename T> inline const T* array_id(const std::vector<T>& 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<Color> values;
};
// GX colour array formats: RGB565, RGB8, RGBX8, RGBA4, RGBA6, RGBA8.
inline std::vector<ColorArray> color_arrays(Reader r,size_t model,uint32_t dictionary_offset) {
std::vector<ColorArray> 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<sizes[format] || !r.has(data,size_t(count)*stride)) return {};
ColorArray a; a.id=r.u32(h+0x10);a.values.reserve(count);
for(uint32_t i=0;i<count;++i) {
const size_t p=data+size_t(i)*stride;
const uint32_t v16=r.u16(p),v24=(uint32_t(r.u16(p))<<8)|r.u8(p+2);
Color c{1,1,1,1};
switch(format) {
case 0: c={((v16>>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<float,6> 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<V2,2> uv{}; };
struct Texture {
std::string name;
uint16_t width=0,height=0;
uint32_t format=0,mips=1;
std::vector<uint8_t> bytes;
std::vector<uint8_t> 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<float,6> 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<Stage,4> stages{};
std::array<Color,4> registers{}; // PREV, C0, C1, C2
std::array<TexGen,8> texGens{};
std::array<Map,8> maps{};
};
struct Part {
std::vector<Vertex> vertices;
uint16_t material=0;
bool translucent=false;
// Billboards keep bone-local positions around origin and face the eye.
bool billboard=false;
V3 origin;
};
struct Model {
std::vector<Part> parts;
std::vector<Material> materials;
std::vector<Texture> textures;
V3 minimum{std::numeric_limits<float>::max(),std::numeric_limits<float>::max(),std::numeric_limits<float>::max()};
V3 maximum{-std::numeric_limits<float>::max(),-std::numeric_limits<float>::max(),-std::numeric_limits<float>::max()};
bool valid() const { return !parts.empty() && maximum.x>=minimum.x; }
};
inline void bounds(Model& model,V3 p) {
model.minimum={std::min(model.minimum.x,p.x),std::min(model.minimum.y,p.y),std::min(model.minimum.z,p.z)};
model.maximum={std::max(model.maximum.x,p.x),std::max(model.maximum.y,p.y),std::max(model.maximum.z,p.z)};
}
inline float signed11(uint32_t v) { int32_t x=int32_t(v&0x7ffu);if(x&0x400) x-=0x800;return float(x)/255.f; }
inline Color konst_value(const std::array<Color,4>& konst,uint32_t sel,bool alpha) {
if(sel<8) { const float v=float(8-sel)/8.f;return {v,v,v,v}; }
if(!alpha && sel>=0x0c && sel<=0x0f) { const auto& k=konst[sel-0x0c];return {k[0],k[1],k[2],k[3]}; }
if(sel>=0x10 && sel<=0x1f) { const float v=konst[sel&3][(sel-0x10)>>2];return {v,v,v,v}; }
return {0,0,0,0};
}
// Walks a G3D display list of BP (0x61), XF (0x10) and CP (0x08) loads.
template <typename Bp,typename Xf>
inline void walk_dl(Reader r,size_t at,size_t end,Bp&& bp,Xf&& xf) {
end=std::min(end,r.size);
while(at<end) {
const uint8_t op=r.u8(at++);
if(op==0) continue;
if(op==0x61 && at+4<=end) { bp(r.u8(at),r.u32(at)&0xffffffu);at+=4; }
else if(op==0x10 && at+4<=end) {
const size_t count=size_t(r.u16(at))+1;const uint16_t address=r.u16(at+2);at+=4;
for(size_t i=0;i<count && at+4<=end;++i,at+=4) xf(uint32_t(address+i),r.u32(at));
} else if(op==0x08 && at+5<=end) at+=5;
else return;
}
}
inline int texture_index(Reader r,const std::vector<Entry>& fileTextures,const std::string& name,Model& model) {
for(size_t i=0;i<model.textures.size();++i) if(model.textures[i].name==name) return int(i);
const size_t tex=find(fileTextures,name);
if(!tex || !r.has(tex,0x40) || r.u32(tex)!=0x54455830u) return -1;
const size_t end=tex+r.u32(tex+4),start=tex+r.u32(tex+0x10);
const uint16_t width=r.u16(tex+0x1c),height=r.u16(tex+0x1e);
const uint32_t format=r.u32(tex+0x20),mips=std::clamp(r.u32(tex+0x24),1u,11u);
if(end>r.size || start>=end || !width || !height || width>1024 || height>1024 ||
format>14 || format==7 || (format>=8 && format<=13)) return -1;
model.textures.push_back({name,width,height,format,mips,std::vector<uint8_t>(r.bytes+start,r.bytes+end),{}});
return int(model.textures.size()-1);
}
inline bool parse_material(Reader r,size_t mat,const std::vector<Entry>& fileTextures,Model& model,Material& out) {
if(!r.has(mat,0x418)) return false;
const uint8_t genCount=std::min<uint8_t>(r.u8(mat+0x14),8);
out.cull=uint8_t(r.u32(mat+0x18)&3u);
const uint32_t layers=r.u32(mat+0x2c);
const size_t layerAt=r.rel(mat,mat+0x30);
if(layers>8 || (layers && (!layerAt || !r.has(layerAt,size_t(layers)*0x34)))) return false;
for(uint32_t i=0;i<layers;++i) {
const size_t layer=layerAt+size_t(i)*0x34;
const uint32_t map=r.u32(layer+0x10);
if(map>=8) return false;
out.maps[map]={texture_index(r,fileTextures,r.str(r.rel(layer,layer)),model),
uint8_t(std::min(r.u32(layer+0x18),2u)),uint8_t(std::min(r.u32(layer+0x1c),2u))};
}
const uint32_t srtMode=r.u32(mat+0x1ac);
for(uint8_t i=0;i<genCount;++i) {
const size_t srt=mat+0x1b0+size_t(i)*20;
out.texGens[i].matrix=texture_srt(r.f32(srt),r.f32(srt+4),r.f32(srt+8),r.f32(srt+12),r.f32(srt+16),srtMode);
const uint8_t mapMode=r.u8(mat+0x250+size_t(i)*0x34+2);
out.texGens[i].normal=mapMode!=0;
}
const size_t channel=mat+0x3f0;
out.materialColor={r.u8(channel+4)/255.f,r.u8(channel+5)/255.f,r.u8(channel+6)/255.f,r.u8(channel+7)/255.f};
out.colorControl=r.u32(channel+0xc);out.alphaControl=r.u32(channel+0x10);
std::array<Color,4> konst{};
std::array<uint8_t,4> kc{},ka{};
kc.fill(0x0c);ka.fill(0x1c);
const auto bp=[&](uint8_t reg,uint32_t v) {
if(reg==0xf3) out.alphaCompare=v;
else if(reg==0x40) out.depthWrite=(v>>4)&1u;
else if(reg==0x41) {
out.blend=v&1u;out.blendDst=(v>>5)&7u;out.blendSrc=(v>>8)&7u;out.subtract=(v>>11)&1u;
} else if(reg>=0xe0 && reg<=0xe7) {
const bool hi=reg&1u;
auto& c=(v>>23)?konst[(reg-0xe0)>>1]:out.registers[(reg-0xe0)>>1];
const float low=(v>>23)?float(v&0xffu)/255.f:signed11(v),high=(v>>23)?float((v>>12)&0xffu)/255.f:signed11(v>>12);
if(hi) { c[2]=low;c[1]=high; } else { c[0]=low;c[3]=high; }
} else if(reg>=0x28 && reg<=0x29) {
for(uint32_t half=0;half<2;++half) {
auto& s=out.stages[(reg-0x28)*2+half];
const uint32_t x=v>>(12*half);
s.texMap=x&7u;s.texCoord=(x>>3)&7u;s.textured=(x>>6)&1u;s.rasterized=((x>>7)&7u)==0;
}
} else if(reg>=0xc0 && reg<=0xc7) {
auto& s=out.stages[(reg-0xc0)>>1];
(reg&1u?s.alpha:s.color)=v;
} else if(reg>=0xf6 && reg<=0xf7) {
for(uint32_t half=0;half<2;++half) {
const size_t stage=(reg-0xf6)*2+half;
kc[stage]=(v>>(4+10*half))&31u;ka[stage]=(v>>(9+10*half))&31u;
}
}
};
const auto xf=[&](uint32_t address,uint32_t v) {
if(address<0x1040 || address>=0x1040u+genCount) return;
// TEXMTXINFO source row: 1 is the normal, 5..12 are UV sets.
const uint32_t row=(v>>7)&31u;
auto& gen=out.texGens[address-0x1040];
if(row>=5 && row<=12 && !gen.normal) gen.uvSet=uint8_t(std::min(row-5,1u));
else gen.normal=true;
};
const size_t tev=r.rel(mat,mat+0x28),dl=r.rel(mat,mat+0x3c);
if(!tev || !dl || !r.has(tev,0x20)) return false;
walk_dl(r,dl,dl+0x180,bp,xf);
walk_dl(r,tev+0x20,tev+std::min<size_t>(r.u32(tev),0x400),bp,xf);
out.stageCount=std::min<uint8_t>(r.u8(tev+0xc),4);
for(uint8_t i=0;i<out.stageCount;++i) {
auto& s=out.stages[i];
const Color c=konst_value(konst,kc[i],false),a=konst_value(konst,ka[i],true);
s.konst={c[0],c[1],c[2],a[3]};
s.textured=s.textured && out.maps[s.texMap].texture>=0;
if(s.texCoord>=genCount) s.texCoord=0;
}
return out.stageCount>0;
}
struct Bone { Matrix matrix;uint32_t billboard=0;bool valid=false; };
inline bool decode_shape(Reader r,size_t shape,const std::vector<Array>& positions,
const std::vector<Array>& normals,const std::vector<ColorArray>& colors,
const std::vector<Array>& uvs,const std::vector<Bone>& bones,
Part& part,Model& model) {
if(!r.has(shape,0x68)) return false;
const uint32_t lo=r.u32(shape+0xc),hi=r.u32(shape+0x10);
const int desc[12]{int((lo>>9)&3),int((lo>>11)&3),int((lo>>13)&3),int((lo>>15)&3),
int(hi&3),int((hi>>2)&3),int((hi>>4)&3),int((hi>>6)&3),int((hi>>8)&3),
int((hi>>10)&3),int((hi>>12)&3),int((hi>>14)&3)};
if(desc[0]<2) return false;
const auto* pos=array_id(positions,r.u16(shape+0x48));
const auto* nrm=desc[1]?array_id(normals,r.u16(shape+0x4a)):nullptr;
const auto* clr=desc[2]?array_id(colors,r.u16(shape+0x4c)):nullptr;
const Array* uv[2]{desc[4]?array_id(uvs,r.u16(shape+0x50)):nullptr,
desc[5]?array_id(uvs,r.u16(shape+0x52)):nullptr};
if(!pos || (desc[1] && !nrm) || (desc[2] && !clr) || (desc[4] && !uv[0]) || (desc[5] && !uv[1])) return false;
size_t matrix_bytes=0;
for(uint32_t mask=lo&511;mask;mask>>=1) matrix_bytes+=mask&1u;
size_t stride=matrix_bytes;
for(int d:desc) {
if(d==1) return false;
stride+=d==2?1:d==3?2:0;
}
const size_t begin=shape+0x24+r.u32(shape+0x2c),length=r.u32(shape+0x28);
if(stride<2 || length>65536 || !r.has(begin,length)) return false;
const size_t end=begin+length;
// Matrix IDs, not bone indices: a single-bound shape names its own matrix;
// one with PNMTXIDX loads a palette of them. Envelope IDs have no bone and are
// identity in the bind pose (their vertices are already in model space).
const int32_t single=int32_t(r.u32(shape+8));
const auto resolve=[&](uint32_t id) -> const Bone* { return id<bones.size() && bones[id].valid?&bones[id]:nullptr; };
const Bone identity{};
const Bone* singleBone=single>=0?resolve(uint32_t(single)):nullptr;
if(!(lo&1u) && singleBone && singleBone->billboard) {
part.billboard=true;
const auto& m=singleBone->matrix.v;
part.origin={m[3],m[7],m[11]};
}
std::array<uint16_t,10> palette{};
palette.fill(uint16_t(std::max(single,0)));
size_t at=begin;
while(at<end) {
const uint8_t op=r.u8(at++);
if(op==0) continue;
if(op==0x20 || op==0x28 || op==0x30 || op==0x38) {
if(at+4>end) return false;
const uint32_t slot=(r.u16(at+2)&0xfffu)/12u;
if(op==0x20 && slot<palette.size()) palette[slot]=r.u16(at);
at+=4;continue;
}
const uint8_t primitive=op&0xf8;
if((primitive!=0x80 && primitive!=0x90 && primitive!=0x98 && primitive!=0xa0) || at+2>end) return false;
const uint16_t count=r.u16(at);at+=2;
if(count>8192 || size_t(count)*stride>end-at) return false;
std::vector<Vertex> source;
source.reserve(count);
for(uint16_t i=0;i<count;++i) {
const Bone* bone=singleBone;
if(lo&1u) {
const uint32_t slot=r.u8(at)/3u;
bone=slot<palette.size()?resolve(palette[slot]):nullptr;
}
at+=matrix_bytes;
uint16_t indices[12]{};
for(int a=0;a<12;++a) {
if(desc[a]==2) indices[a]=r.u8(at++);
else if(desc[a]==3) { indices[a]=r.u16(at);at+=2; }
}
if(indices[0]>=pos->values.size() || (nrm && indices[1]>=nrm->values.size()) ||
(clr && indices[2]>=clr->values.size()) || (uv[0] && indices[4]>=uv[0]->values.size()) ||
(uv[1] && indices[5]>=uv[1]->values.size())) return false;
const Matrix& transform=(bone?bone:&identity)->matrix;
Vertex v;
v.normal=nrm?nrm->values[indices[1]]:V3{0,1,0};
if(part.billboard) {
// Keep the bone's scale; the renderer replaces its rotation.
const auto& m=transform.v;
const V3 p=pos->values[indices[0]];
v.position={p.x*std::hypot(m[0],m[4],m[8]),p.y*std::hypot(m[1],m[5],m[9]),p.z*std::hypot(m[2],m[6],m[10])};
} else {
v.position=point(transform,pos->values[indices[0]]);
v.normal=direction(transform,v.normal);
}
if(clr) v.color=clr->values[indices[2]];
for(int set=0;set<2;++set) if(uv[set]) {
const V3 t=uv[set]->values[indices[4+set]];
v.uv[set]={t.x,t.y};
}
source.push_back(v);
}
const auto tri=[&](uint16_t a,uint16_t b,uint16_t c) {
if(a==b || b==c || a==c) return;
for(uint16_t i:{a,b,c}) part.vertices.push_back(source[i]);
};
if(primitive==0x90) { for(uint16_t i=0;i+2<count;i+=3) tri(i,i+1,i+2); }
else if(primitive==0x80) { for(uint16_t i=0;i+3<count;i+=4) {tri(i,i+1,i+2);tri(i,i+2,i+3);} }
else if(primitive==0x98) { for(uint16_t i=2;i<count;++i) {
if(i&1) tri(i-1,i-2,i);else tri(i-2,i-1,i);
} }
else { for(uint16_t i=2;i<count;++i) tri(0,i-1,i); }
}
for(const auto& v:part.vertices) {
if(!part.billboard) { bounds(model,v.position);continue; }
const float radius=std::sqrt(v.position.x*v.position.x+v.position.y*v.position.y+v.position.z*v.position.z);
bounds(model,{part.origin.x-radius,part.origin.y-radius,part.origin.z-radius});
bounds(model,{part.origin.x+radius,part.origin.y+radius,part.origin.z+radius});
}
return true;
}
inline Model parse_model(Reader r,const std::string& model_name) {
Model result;
if(!r.has(0,16) || r.u32(0)!=0x62726573u) return result;
const auto groups=dict(r,r.u16(12)+8);
const size_t model_dict=find(groups,"3DModels(NW4R)");
const size_t texture_dict=find(groups,"Textures(NW4R)");
if(!model_dict || !texture_dict) return result;
const size_t m=find(dict(r,model_dict),model_name);
if(!m || !r.has(m,0x40) || r.u32(m)!=0x4d444c30u || r.u32(m+8)!=11) return result;
const size_t model_end=m+r.u32(m+4);
if(model_end>r.size || model_end<=m) return result;
const auto joint_entries=dict(r,m+r.u32(m+0x14));
const auto material_entries=dict(r,m+r.u32(m+0x30));
const auto shape_entries=dict(r,m+r.u32(m+0x38));
const auto file_textures=dict(r,texture_dict);
const auto draw_entries=dict(r,m+r.u32(m+0x10));
if(joint_entries.empty() || material_entries.empty() || shape_entries.empty() ||
joint_entries.size()>256 || shape_entries.size()>256 || material_entries.size()>64) return result;
// Each bone stores its bind-pose model matrix; index it by matrix ID, the
// number shapes and palettes use (NodeTree parents are matrix IDs too).
std::vector<Bone> bones;
for(const auto& e:joint_entries) {
if(!r.has(e.at,0xa0)) return {};
const uint32_t id=r.u32(e.at+0x10);
if(id>=1024) return {};
if(id>=bones.size()) bones.resize(id+1);
auto& bone=bones[id];
for(int i=0;i<12;++i) bone.matrix.v[i]=r.f32(e.at+0x70+size_t(i)*4);
for(float f:bone.matrix.v) if(!std::isfinite(f)) return {};
bone.billboard=r.u32(e.at+0x18);bone.valid=true;
}
const auto positions=arrays(r,m,r.u32(m+0x18),ArrayKind::Position);
const auto normals=arrays(r,m,r.u32(m+0x1c),ArrayKind::Normal);
const auto colors=color_arrays(r,m,r.u32(m+0x20));
const auto uvs=arrays(r,m,r.u32(m+0x24),ArrayKind::UV);
if(positions.empty()) return {};
struct Draw { uint16_t mat,shape;bool xlu; };
std::vector<Draw> draws;
for(const char* list:{"DrawOpa","DrawXlu"}) {
const size_t start=find(draw_entries,list);
if(!start) continue;
size_t at=start;
for(size_t guard=0;guard<4096 && at<model_end;++guard) {
const uint8_t op=r.u8(at);
if(op==1) break;
if(op!=4 || !r.has(at,8)) return {};
draws.push_back({r.u16(at+1),r.u16(at+3),std::strcmp(list,"DrawXlu")==0});
at+=8;
}
}
if(draws.empty() || draws.size()>512) return {};
std::vector<int> materialSlot(material_entries.size(),-1);
for(const auto& draw:draws) {
if(draw.mat>=material_entries.size() || draw.shape>=shape_entries.size()) return {};
if(materialSlot[draw.mat]<0) {
Material material;
if(!parse_material(r,material_entries[draw.mat].at,file_textures,result,material)) return {};
materialSlot[draw.mat]=int(result.materials.size());
result.materials.push_back(material);
}
Part part;part.translucent=draw.xlu;part.material=uint16_t(materialSlot[draw.mat]);
if(!decode_shape(r,shape_entries[draw.shape].at,positions,normals,colors,uvs,bones,part,result)) return {};
if(!part.vertices.empty()) result.parts.push_back(std::move(part));
}
return result.valid()?result:Model{};
}
inline constexpr std::array<const char*,15> names{"koura_green","koura_red","banana","itemBoxNiseRtpa",
"kinoko","bomb","togezo_koura","thunder","star","kinoko_p","big_kinoko","gesso",
"pow_bloc","kumo","item_killer"};
inline int model_index(uint8_t id) {
constexpr int map[19]{0,1,2,3,4,4,5,6,7,8,9,10,11,12,13,14,0,1,2};
return id<19?map[id]:-1;
}
struct Archive { std::array<Model,15> models;uint32_t loaded=0; };
inline Archive parse_archive(const void* bytes,size_t size) {
Archive archive;
if(!bytes || !size || size>32u*1024u*1024u) return archive;
const auto unpacked=yaz0({static_cast<const uint8_t*>(bytes),size});
Reader r{unpacked.data(),unpacked.size()};
if(!r.has(0,0x20) || r.u32(0)!=0x55aa382du) return archive;
const size_t root=r.u32(4);
if(!r.has(root,12)) return archive;
const uint32_t count=r.u32(root+8);
if(count>8192 || count<2 || !r.has(root,size_t(count)*12)) return archive;
const size_t names_base=root+size_t(count)*12;
for(uint32_t i=1;i<count;++i) {
const size_t e=root+size_t(i)*12,tag=r.u32(e);
if(tag>>24) continue;
const std::string filename=r.str(names_base+(tag&0xffffff));
for(size_t item=0;item<names.size();++item) {
if(filename!=std::string(names[item])+".brres") continue;
const size_t at=r.u32(e+4),length=r.u32(e+8);
if(!r.has(at,length) || length>4u*1024u*1024u) break;
const Reader file{r.bytes+at,length};
const std::string model_name=item==3?"itemBoxNise":names[item];
archive.models[item]=parse_model(file,model_name);
if(archive.models[item].valid()) ++archive.loaded;
break;
}
}
return archive;
}
} // namespace aurora::gfx::cockpit_item::data
+4
View File
@@ -3122,3 +3122,7 @@ void aurora_set_vr_hand_mesh(uint32_t hand, const AuroraVRHandVertex* vertices,
meshes[hand] = std::move(mesh);
++meshRevision;
}
void aurora_set_cockpit_item_archive(const void* bytes, uint32_t size) {
aurora::gfx::cockpit_item::set_archive(bytes, size);
}
+1
View File
@@ -321,6 +321,7 @@ struct StereoReplayFrame {
std::array<StereoReplayEye, AURORA_STEREO_EYE_COUNT> eyes;
// VR hands and synthetic wheel, drawn per eye after the world (gfx/cockpit.hpp).
AuroraCockpit cockpit{};
AuroraCockpitItem cockpitItem{};
// The immersive window (AuroraStereoFrame::window): each eye is masked to the
// 2D layer's screen after its last draw (gfx/window_mask.hpp).
bool window = false;
+7 -1
View File
@@ -1,6 +1,12 @@
include(FetchContent)
include(GoogleTest)
# The parser test uses synthetic malformed inputs by default. Passing the
# user's Race/Common.szs additionally audits every required model and texture.
add_executable(cockpit_item_data_test cockpit_item_data_test.cpp)
target_compile_features(cockpit_item_data_test PRIVATE cxx_std_20)
add_test(NAME cockpit_item_data_test COMMAND cockpit_item_data_test)
option(AURORA_GPU_SMOKE_TESTS "Build opt-in tests requiring a desktop GPU" OFF)
if (AURORA_GPU_SMOKE_TESTS AND AURORA_ENABLE_GX AND WIN32)
# Exercises the custom Dawn DLL's Aurora Vulkan ABI (patches/dawn). Run it with that
@@ -29,7 +35,7 @@ if (AURORA_GPU_SMOKE_TESTS AND AURORA_ENABLE_GX AND WIN32)
dawn::dawncpp_headers)
# VR cockpit overlay (hands, synthetic wheel) against real scene depth. Standalone: it
# defines the GPU globals itself and needs only the header.
add_executable(cockpit_gpu_smoke cockpit_gpu_smoke.cpp)
add_executable(cockpit_gpu_smoke cockpit_gpu_smoke.cpp ../lib/gfx/texture_convert.cpp)
target_include_directories(cockpit_gpu_smoke PRIVATE ../include ../lib)
target_compile_definitions(cockpit_gpu_smoke PRIVATE AURORA TARGET_PC WEBGPU_DAWN)
target_link_libraries(cockpit_gpu_smoke PRIVATE fmt::fmt xxhash absl::flat_hash_map absl::btree
+62 -2
View File
@@ -5,10 +5,16 @@
#include "../lib/gfx/cockpit.hpp"
#include <fstream>
#include <iostream>
#include <iterator>
#include <atomic>
namespace aurora::webgpu { wgpu::Device g_device; wgpu::Queue g_queue; GraphicsConfig g_graphicsConfig{}; }
namespace aurora {
AuroraConfig g_config{};
void log_internal(AuroraLogLevel,const char*,const char*,unsigned int) noexcept {}
void Module::show_fatal_dialog(const char*,std::string_view) noexcept {}
}
std::atomic<int> errors=0;
int main() {
int main(int argc,char** argv) {
using namespace aurora;
using namespace webgpu;
wgpu::InstanceDescriptor id{};
@@ -40,6 +46,50 @@ int main() {
if(!gfx::cockpit::geometry(native).empty()) return 1;
native.nativeWheel=false;
if(gfx::cockpit::geometry(native).empty()) return 1;
// The held item stays upright and faces the player whatever the hand's roll
// and pitch; only the hand's heading turns it.
const auto itemFrame=[](const AuroraCockpitHand& hand,float frontX,float frontZ) {
std::array<float,12> item{};
if(!gfx::cockpit_item::seat_from_item(hand,item)) return false;
const bool upright=std::abs(item[1])<1e-5f && std::abs(item[5]-1)<1e-5f && std::abs(item[9])<1e-5f;
return upright && std::abs(item[2]-frontX)<1e-4f && std::abs(item[10]-frontZ)<1e-4f;
};
for(float roll : {0.0f,1.0f,-2.0f}) for(float pitch : {0.0f,0.5f,-0.7f}) {
// Fingers ahead (grip -Y is seat -Z), little finger to thumb up (grip -Z is
// seat +Y), then rolled about the fingers and pitched about seat +X.
const float cr=std::cos(roll),sr=std::sin(roll),cp=std::cos(pitch),sp=std::sin(pitch);
const gfx::cockpit::M neutral{1,0,0,-0.18f, 0,0,-1,-0.30f, 0,1,0,-0.42f};
const gfx::cockpit::M rollZ{cr,-sr,0,0, sr,cr,0,0, 0,0,1,0}, pitchX{1,0,0,0, 0,cp,-sp,0, 0,sp,cp,0};
AuroraCockpitHand hand{};
const auto pose=gfx::cockpit::compose(pitchX,gfx::cockpit::compose(rollZ,neutral));
std::memcpy(hand.seatFromGrip,pose.data(),sizeof(hand.seatFromGrip));
if(!itemFrame(hand,0,1)) { std::cerr<<"Held item not upright or not facing the player\n"; return 1; }
}
{
AuroraCockpitHand hand{};
// A tracked palm joint with the fingers (-Z) ahead, then a grip turned to the right.
const auto palm=gfx::cockpit::identity();
std::memcpy(hand.seatFromJoint[0],palm.data(),sizeof(hand.seatFromJoint[0]));
hand.jointsValid=true;
if(!itemFrame(hand,0,1)) { std::cerr<<"Held item ignores the palm joint\n"; return 1; }
hand.jointsValid=false;
const gfx::cockpit::M right{0,-1,0,0, 0,0,-1,0, 1,0,0,0};
std::memcpy(hand.seatFromGrip,right.data(),sizeof(hand.seatFromGrip));
if(!itemFrame(hand,-1,0)) { std::cerr<<"Held item does not turn with the hand\n"; return 1; }
}
const bool itemEnabled=argc>1;
if(itemEnabled) {
std::ifstream file(argv[1],std::ios::binary);
if(!file) return 1;
const std::vector<uint8_t> bytes{std::istreambuf_iterator<char>(file),std::istreambuf_iterator<char>()};
gfx::cockpit_item::set_archive(bytes.data(),static_cast<uint32_t>(bytes.size()));
for(uint8_t id=0;id<19;++id) if(!gfx::cockpit_item::has_model(id)) return 1;
for(const auto& model:gfx::cockpit_item::archive->models)
for(const auto& texture:model.textures)
if(texture.rgba.size()!=gfx::cockpit_item::mip_bytes(texture,texture.mips)) {
std::cerr << "Cockpit item texture conversion failed: " << texture.name << '\n';return 1;
}
}
for(bool bike : {false,true}) for(bool original : {false,true}) for(uint32_t samples : {1u,4u})
for(bool hud : {false,true}) for(int coverage : {0,1,2}) for(bool reversed : {false,true}) for(uint32_t eyeIndex : {0u,1u}) {
const bool occluded=coverage==1;
@@ -48,6 +98,16 @@ int main() {
frame.cockpit.active=true;frame.cockpit.wheelAngle=0.35f;
frame.cockpit.nativeWheel=original;
frame.cockpit.bike=bike;frame.cockpit.handlebarRadius=0.25f;
if(itemEnabled) {
const uint32_t itemCase=((((uint32_t(bike)*2+uint32_t(original))*2+
uint32_t(samples==4))*3+uint32_t(coverage))*2+
uint32_t(reversed))*2+eyeIndex;
const bool preview=samples==4 && !bike && !original && coverage==0 &&
!reversed && eyeIndex==0;
const uint8_t id=preview?0:static_cast<uint8_t>(itemCase%19);
frame.cockpitItem={1,id,static_cast<uint8_t>(itemCase%3+1),
static_cast<uint8_t>(itemCase%2),true};
}
const float handlePose[12]{1,0,0,0, 0,0,1,-0.3f, 0,-1,0,-0.42f};
std::memcpy(frame.cockpit.seatFromHandlebar,handlePose,sizeof(handlePose));
for(int hand=0;hand<2;++hand) {
@@ -156,7 +216,7 @@ int main() {
}
if(left<500||right>8) { std::cerr<<"Partial wall occlusion failed for eye "<<eyeIndex<<'\n';++errors; }
}
if(samples==4 && !original && !occluded) {
if(samples==4 && !bike && !original && !hud && coverage==0 && !reversed && eyeIndex==0) {
std::ofstream image("cockpit-preview.ppm",std::ios::binary);image<<"P6\n512 512\n255\n";
for(size_t i=0;i<512*512;++i) image.write(reinterpret_cast<const char*>(bytes+i*4),3);
}
@@ -0,0 +1,75 @@
#include "../lib/gfx/cockpit_item_data.hpp"
#include <cmath>
#include <cstdlib>
#include <fstream>
#include <iostream>
#include <iterator>
static void Check(bool value,const char* what) {
if (!value) { std::cerr << "FAILED: " << what << '\n'; std::abort(); }
}
static bool Near(float a,float b) { return std::fabs(a-b)<1e-4f; }
int main(int argc, char** argv) {
using namespace aurora::gfx::cockpit_item::data;
for (uint8_t id=0; id<19; ++id) Check(model_index(id)>=0,"every inventory ID has a model");
Check(model_index(19)==-1 && model_index(20)==-1,"no model past the triple banana");
const uint8_t brokenYaz[]{'Y','a','z','0',0,0,0,4,0,0,0,0,0,0,0,0,0};
const uint8_t brokenU8[]{0x55,0xaa,0x38,0x2d,0,0,0,0};
Check(parse_archive(nullptr,0).loaded==0,"null archive");
Check(parse_archive(brokenYaz,sizeof(brokenYaz)).loaded==0,"truncated Yaz0");
Check(parse_archive(brokenU8,sizeof(brokenU8)).loaded==0,"truncated U8");
// Maya texture SRT: a 2x scale pivots t around 1, as G3D does.
const auto srt=texture_srt(2,2,0,0,0,0);
Check(Near(srt[0],2) && Near(srt[1],0) && Near(srt[2],0) &&
Near(srt[3],0) && Near(srt[4],2) && Near(srt[5],-1),"Maya scale");
const auto identity=texture_srt(1,1,0,0,0,0);
Check(Near(identity[0],1) && Near(identity[2],0) && Near(identity[4],1) && Near(identity[5],0),"identity SRT");
// KSEL: fixed fractions, whole konst colours, and single konst components.
const std::array<Color,4> konst{{{0.1f,0.2f,0.3f,0.4f},{0.5f,0.6f,0.7f,0.8f},{},{}}};
Check(Near(konst_value(konst,0,false)[0],1) && Near(konst_value(konst,4,true)[3],0.5f),"KSEL fractions");
Check(Near(konst_value(konst,0x0d,false)[1],0.6f),"KSEL konst colour");
Check(Near(konst_value(konst,0x1c,true)[3],0.4f) && Near(konst_value(konst,0x15,true)[3],0.6f),"KSEL components");
Check(Near(signed11(0x7ff),-1.f/255.f) && Near(signed11(0xff),1),"TEV register sign");
if (argc>1) {
std::ifstream file(argv[1],std::ios::binary);
Check(bool(file),"archive readable");
const std::vector<uint8_t> bytes{std::istreambuf_iterator<char>(file),std::istreambuf_iterator<char>()};
const Archive archive=parse_archive(bytes.data(),bytes.size());
Check(archive.loaded==15,"all 15 item models");
for (size_t i=0;i<archive.models.size();++i) {
const auto& model=archive.models[i];
Check(model.valid(),"model valid");
size_t vertices=0,billboards=0;
for (const auto& part:model.parts) {
Check(part.vertices.size()%3==0,"triangle list");
Check(part.material<model.materials.size(),"part material");
vertices+=part.vertices.size();
billboards+=part.billboard;
}
for (const auto& material:model.materials) {
Check(material.stageCount>=1 && material.stageCount<=4,"TEV stage count");
for (uint32_t s=0;s<material.stageCount;++s) {
const auto& stage=material.stages[s];
if (stage.textured) Check(material.maps[stage.texMap].texture>=0,"stage texture resolves");
}
}
Check(vertices>0,"geometry");
std::cout << names[i] << ": " << vertices << " vertices, " << model.textures.size() << " textures, "
<< model.materials.size() << " materials, " << billboards << " billboard parts\n";
}
// The properties whose absence corrupted the held items.
const auto& shell=archive.models[0].materials[0];
Check(shell.maps[0].wrapS==2 && Near(shell.texGens[0].matrix[0],2),"shell mirror wrap and 2x SRT");
Check(shell.texGens[1].normal,"shell specular is an env map");
Check(archive.models[2].materials[0].cull==0,"banana is double-sided");
Check(archive.models[3].parts.size()==3,"fake item box parts");
size_t thunderBillboards=0;
for (const auto& part:archive.models[7].parts) thunderBillboards+=part.billboard;
Check(thunderBillboards==1,"lightning glow is a billboard");
const auto& bolt=archive.models[7].materials[archive.models[7].parts[0].material];
Check(Near(bolt.registers[1][0],1) && Near(bolt.registers[1][1],1) && Near(bolt.registers[1][2],0),
"lightning C0 is yellow");
}
}
+5
View File
@@ -456,6 +456,11 @@ target_include_directories(mkw_vr_player_tests PRIVATE "${CMAKE_CURRENT_LIST_DIR
target_compile_features(mkw_vr_player_tests PRIVATE cxx_std_17)
add_test(NAME mkw_vr_player_tests COMMAND mkw_vr_player_tests)
add_executable(mkw_vr_item_tests "${CMAKE_CURRENT_LIST_DIR}/tests/vr_item_tests.cpp")
target_include_directories(mkw_vr_item_tests PRIVATE "${CMAKE_CURRENT_LIST_DIR}/include")
target_compile_features(mkw_vr_item_tests PRIVATE cxx_std_17)
add_test(NAME mkw_vr_item_tests COMMAND mkw_vr_item_tests)
add_executable(mkw_vr_policy_tests tests/vr_policy_tests.cpp src/vr/mkw_vr_policy.cpp)
target_include_directories(mkw_vr_policy_tests PRIVATE "${CMAKE_CURRENT_LIST_DIR}/include")
target_compile_features(mkw_vr_policy_tests PRIVATE cxx_std_17)
+9
View File
@@ -0,0 +1,9 @@
// SPDX-License-Identifier: GPL-3.0-or-later
#pragma once
#include <cstdint>
#include <vector>
// Reads the same mapped disc path that the guest sees, including active file
// replacements. Called on the guest thread after DVD initialization.
std::vector<uint8_t> DVDReadVrAsset(const char* dvdPath);
+16
View File
@@ -78,6 +78,7 @@ struct RuntimeUserConfig {
std::optional<bool> vrNativeSteeringWheel;
std::optional<bool> vrObjectCulling;
std::optional<bool> vrHandSteering;
std::optional<std::string> vrCockpitItemHand;
std::optional<bool> vrHandTracking;
std::optional<float> vrWheelKartDegrees;
std::optional<float> vrWheelBikeDegrees;
@@ -237,6 +238,7 @@ inline constexpr float kVrCockpitUnitsPerMeterMax = 400.0f;
inline constexpr bool kVrSteeringWheelDefault = true;
inline constexpr bool kVrNativeSteeringWheelDefault = true;
inline constexpr bool kVrHandSteeringDefault = true;
inline constexpr const char* kVrCockpitItemHandDefault = "left";
// The game hides karts and objects its own camera cannot see, which a head
// turn in VR reveals. object_culling false draws them anyway (see
// vr/mkw_vr_culling.h); it only takes effect while VR is enabled. The PC
@@ -605,6 +607,8 @@ inline void EnsureConfigFile() {
"# (seconds) a hand that loses tracking keeps hold, and a short\n"
"# pulse on grab and release. All changeable live from the F10 menu.\n"
"hand_steering = true\n"
"# Show the settled inventory item in one cockpit hand: left, right, or off.\n"
"cockpit_item_hand = \"left\"\n"
"wheel_kart_degrees = 90.0\n"
"wheel_bike_degrees = 45.0\n"
"wheel_grab_distance = 0.35\n"
@@ -880,6 +884,7 @@ inline RuntimeUserConfig ParseConfigDocument(const toml::value& document) {
config.vrNativeSteeringWheel = FindConfigValue<bool>(document, "vr", "native_steering_wheel");
config.vrObjectCulling = FindConfigValue<bool>(document, "vr", "object_culling");
config.vrHandSteering = FindConfigValue<bool>(document, "vr", "hand_steering");
config.vrCockpitItemHand = FindConfigValue<std::string>(document, "vr", "cockpit_item_hand");
config.vrHandTracking = FindConfigValue<bool>(document, "vr", "hand_tracking");
config.vrWheelKartDegrees = readRangedFloat("wheel_kart_degrees", kVrWheelDegreesMin, kVrWheelDegreesMax);
config.vrWheelBikeDegrees = readRangedFloat("wheel_bike_degrees", kVrWheelDegreesMin, kVrWheelDegreesMax);
@@ -1282,6 +1287,12 @@ inline bool SetVrHandSteering(bool value) {
return WriteSetting("vr", "hand_steering", value ? "true" : "false");
}
inline bool SetVrCockpitItemHand(const std::string& value) {
if (value != "left" && value != "right" && value != "off") return false;
Mutable().vrCockpitItemHand = value;
return WriteSetting("vr", "cockpit_item_hand", "\"" + value + "\"");
}
inline bool SetVrHandTracking(bool value) {
Mutable().vrHandTracking = value;
return WriteSetting("vr", "hand_tracking", value ? "true" : "false");
@@ -1773,6 +1784,11 @@ inline bool VrHandSteering(bool fallback = kVrHandSteeringDefault) {
return Get().vrHandSteering.value_or(fallback);
}
inline std::string VrCockpitItemHand() {
const std::string value = Get().vrCockpitItemHand.value_or(kVrCockpitItemHandDefault);
return value == "left" || value == "right" || value == "off" ? value : kVrCockpitItemHandDefault;
}
inline bool VrHandTracking(bool fallback = kVrHandTrackingDefault) {
return Get().vrHandTracking.value_or(fallback);
}
+5
View File
@@ -3,6 +3,7 @@
#pragma once
#include "vr/steering_wheel.h"
#include "vr/mkw_vr_item.h"
#include <algorithm>
#include <array>
@@ -572,4 +573,8 @@ void MkwVRFirstPersonRecenter() noexcept;
// anchor has been missing long enough to give up holding the last one.
FirstPersonAnchor MkwVRFirstPersonGetAnchor() noexcept;
// Guest-frame inventory snapshot, sampled at the race draw boundary. A
// generation change invalidates any item retained by a prior race.
HeldItem MkwVRFirstPersonGetHeldItem() noexcept;
} // namespace mkw::vr
+47
View File
@@ -0,0 +1,47 @@
// SPDX-License-Identifier: GPL-3.0-or-later
#pragma once
#include <cstdint>
namespace mkw::vr {
struct HeldItem {
uint8_t id = 0x14;
uint8_t count = 0;
bool valid = false;
uint64_t race_generation = 0;
};
namespace detail {
// PAL RMCP01 Item::Manager and Item::Player. Item::PlayerInventory is at
// Player+0x88. Read the inventory, never PlayerRoulette::nextItemId: roulette
// teardown clears the latter, and an interrupted roulette may predict an item
// the player never receives.
template <typename GuestMemory>
HeldItem ReadHeldItem(uint32_t local_racer, uint64_t race_generation) noexcept {
HeldItem result{};
result.race_generation = race_generation;
if (local_racer >= 12) return result;
uint32_t manager = 0, players = 0;
if (!GuestMemory::TryRead32(0x809C3618u, manager) || !manager ||
!GuestMemory::TryRead32(manager + 0x14u, players) || !players) return result;
const uint32_t player = players + local_racer * 0x248u;
if (player < players || !GuestMemory::Contains(player, 0x94u)) return result;
try {
if (GuestMemory::Read8(player + 0x18u) != local_racer ||
GuestMemory::Read32(player + 0x58u) != 0) return result;
const uint32_t id = GuestMemory::Read32(player + 0x8Cu);
const uint32_t count = GuestMemory::Read32(player + 0x90u);
if (id > 0x12u || count == 0 || count > 3) return result;
result.id = static_cast<uint8_t>(id);
result.count = static_cast<uint8_t>(count);
result.valid = true;
} catch (const typename GuestMemory::AccessViolation&) {
return result;
}
return result;
}
} // namespace detail
} // namespace mkw::vr
+15
View File
@@ -1,6 +1,7 @@
#include "hle_stubs.h"
#include "isa/big_endian.h"
#include "hle/dvd_contract.h"
#include "hle/dvd_vr_asset.h"
#include "hle/runtime_parse_helpers.h"
#include "memory.h"
@@ -743,6 +744,20 @@ extern "C" const char* DVDResolveHostPathForTest(const char* dvdPath)
return resolved.c_str();
}
std::vector<uint8_t> DVDReadVrAsset(const char* dvdPath) {
if (dvdPath == nullptr || dvdPath[0] == '\0') return {};
DVDInit_8015EA1C();
const auto it = g_pathToEntry.find(NormalizePath(dvdPath));
if (it == g_pathToEntry.end() || it->second < 0 ||
it->second >= static_cast<int32_t>(g_fileEntries.size())) return {};
const DVDFileEntry& entry = g_fileEntries[it->second];
if (entry.isDirectory || entry.size == 0 || entry.size > 32u * 1024u * 1024u) return {};
std::vector<uint8_t> bytes(entry.size);
std::ifstream file(entry.hostPath, std::ios::binary);
if (!file.read(reinterpret_cast<char*>(bytes.data()), bytes.size())) return {};
return bytes;
}
// ============================================================================
// High-Level DVD API
// ============================================================================
+10
View File
@@ -12,6 +12,7 @@
#include "fiber_manager.h"
#include "platform/host_platform.h"
#include "runtime_log.h"
#include "runtime_config.h"
#include "vr/mkw_vr_first_person.h"
#include "vr/mkw_vr_policy.h"
#include "vr/openxr_integration.h"
@@ -592,6 +593,7 @@ struct GxPresentRecord {
std::array<float, 12> anchor{};
bool anchorValid = false;
float anchorUnitsPerMeter = 0.0f;
AuroraCockpitItem cockpitItem{};
bool reportPaced = false;
bool paced = false;
uint32_t localPlayerCount = 1;
@@ -609,6 +611,7 @@ void GxPresent_gx(GxPresentRecord record) {
} else {
aurora_set_stereo_scene_anchor(record.anchorValid ? record.anchor.data() : nullptr);
}
aurora_set_stereo_cockpit_item(&record.cockpitItem);
aurora_set_stereo_local_player_count(record.localPlayerCount);
aurora_end_frame_ex(record.contentTag, record.imguiFrame);
g_auroraFrameActive.store(false, std::memory_order_release);
@@ -693,6 +696,13 @@ void VI_HLE_PresentFrame(bool presentedXfb, bool paceToRetrace) {
record.anchor = anchor.anchor;
record.anchorValid = anchor.valid;
record.anchorUnitsPerMeter = anchor.unitsPerMeter;
if (anchor.valid) {
const auto item = mkw::vr::MkwVRFirstPersonGetHeldItem();
const auto hand = RuntimeConfigFile::VrCockpitItemHand();
record.cockpitItem = {item.race_generation, item.id, item.count,
static_cast<uint8_t>(hand == "right" ? 1 : hand == "off" ? 2 : 0),
item.valid && hand != "off"};
}
// Latch the current policy safety state into this exact Aurora job. The
// asynchronous worker may ask for an XR packet after the guest has already
// begun the next frame, so immersive replay is accepted only when both
+16 -3
View File
@@ -170,6 +170,9 @@ bool g_vrSteeringWheel = RuntimeConfigFile::VrSteeringWheel();
bool g_vrNativeSteeringWheel = RuntimeConfigFile::VrNativeSteeringWheel();
bool g_vrObjectCulling = RuntimeConfigFile::VrObjectCulling();
bool g_vrHandSteering = RuntimeConfigFile::VrHandSteering();
constexpr std::array<const char*, 3> kVrCockpitItemHands{"Left", "Right", "Off"};
int g_vrCockpitItemHand = RuntimeConfigFile::VrCockpitItemHand() == "right" ? 1 :
RuntimeConfigFile::VrCockpitItemHand() == "off" ? 2 : 0;
mkw::vr::WheelTuning g_vrWheelTuning = RuntimeConfigFile::VrWheelTuning();
float g_vrFirstPersonHeadUp = RuntimeConfigFile::VrFirstPersonHeadUpMeters();
float g_vrFirstPersonHeadForward = RuntimeConfigFile::VrFirstPersonHeadForwardMeters();
@@ -1221,11 +1224,19 @@ void DrawVrSteeringWheelSettings() {
if (ImGui::IsItemHovered()) {
ImGui::SetTooltip("Squeeze a grip near the wheel or handlebar to take hold of it, and turn "
"it to steer, with one hand or both. Releasing both grips gives steering "
"back to the stick, which still aims items. The runtime's hand mesh is "
"used when hand steering was on at launch.");
"back to the stick, which still aims items.");
}
if (ImGui::Combo("Item in cockpit hand", &g_vrCockpitItemHand,
kVrCockpitItemHands.data(), static_cast<int>(kVrCockpitItemHands.size()))) {
RuntimeConfigFile::SetVrCockpitItemHand(
g_vrCockpitItemHand == 1 ? "right" : g_vrCockpitItemHand == 2 ? "off" : "left");
}
if (ImGui::IsItemHovered()) {
ImGui::SetTooltip("Show Player 1's settled inventory item above this palm. "
"Triple items show their remaining count. Using or losing the item hides it.");
}
#if defined(__ANDROID__)
ImGui::BeginDisabled(!g_vrHandSteering);
ImGui::BeginDisabled(!g_vrHandSteering && g_vrCockpitItemHand == 2);
if (ImGui::Checkbox("Tracked hands", &g_vrHandTracking)) {
RuntimeConfigFile::SetVrHandTracking(g_vrHandTracking);
}
@@ -1747,11 +1758,13 @@ void DrawVrCameraSettings() {
g_vrSteeringWheel = RuntimeConfigFile::kVrSteeringWheelDefault;
g_vrNativeSteeringWheel = RuntimeConfigFile::kVrNativeSteeringWheelDefault;
g_vrHandSteering = RuntimeConfigFile::kVrHandSteeringDefault;
g_vrCockpitItemHand = 0;
RuntimeConfigFile::SetVrFirstPersonSeat(RuntimeConfigFile::kVrFirstPersonSeatDefault);
RuntimeConfigFile::SetVrCockpitUnitsPerMeter(g_vrCockpitUnitsPerMeter);
RuntimeConfigFile::SetVrSteeringWheel(g_vrSteeringWheel);
RuntimeConfigFile::SetVrNativeSteeringWheel(g_vrNativeSteeringWheel);
RuntimeConfigFile::SetVrHandSteering(g_vrHandSteering);
RuntimeConfigFile::SetVrCockpitItemHand(RuntimeConfigFile::kVrCockpitItemHandDefault);
#if defined(__ANDROID__)
g_vrHandTracking = RuntimeConfigFile::kVrHandTrackingDefault;
RuntimeConfigFile::SetVrHandTracking(g_vrHandTracking);
+24
View File
@@ -2,7 +2,10 @@
#include "vr/mkw_vr_first_person.h"
#include "aurora/aurora.h"
#include "gx_native_wheel.h"
#include "hle/dvd_vr_asset.h"
#include "gx_model_visibility.h"
#include "memory.h"
#include "runtime_config.h"
@@ -317,6 +320,8 @@ struct FirstPersonState {
uint32_t camera_address = 0;
detail::LocalPlayerKartRead player_kart{};
HeldItem held_item{};
uint64_t race_generation = 0;
// Armed by the draw boundary, consumed by the frame seal.
bool armed = false;
uint64_t armed_frame = 0;
@@ -1386,6 +1391,9 @@ void MkwVRFirstPersonReset() noexcept {
g_state.armed_view_valid = false;
g_state.camera_address = 0;
g_state.player_kart = {};
g_state.held_item = {};
++g_state.race_generation;
g_state.held_item.race_generation = g_state.race_generation;
g_state.anchor = {};
g_state.hold_frames = 0;
g_state.ever_valid_this_race = false;
@@ -1412,6 +1420,8 @@ void MkwVRFirstPersonRecenter() noexcept {
void MkwVRFirstPersonUpdate(uint64_t guest_frame_index, uint32_t race_camera_address) noexcept {
std::lock_guard lock(g_mutex);
g_state.held_item = {};
g_state.held_item.race_generation = g_state.race_generation;
DropHiddenModelsLocked();
if (g_recenter_requested.exchange(false, std::memory_order_acq_rel)) {
g_state.seated_eye.Recalibrate();
@@ -1435,6 +1445,15 @@ void MkwVRFirstPersonUpdate(uint64_t guest_frame_index, uint32_t race_camera_add
g_state.hold_frames = 0;
}
g_state.player_kart = player;
if (player.failed_step == nullptr && g_state.seat == FirstPersonSeat::Cockpit) {
static bool item_archive_attempted = false;
if (!item_archive_attempted && RuntimeConfigFile::VrCockpitItemHand() != "off") {
item_archive_attempted = true;
const auto archive = DVDReadVrAsset("/Race/Common.szs");
if (!archive.empty()) aurora_set_cockpit_item_archive(archive.data(), static_cast<uint32_t>(archive.size()));
}
g_state.held_item = detail::ReadHeldItem<Memory>(player.player_index, g_state.race_generation);
}
g_state.armed = true;
g_state.armed_frame = guest_frame_index;
g_state.armed_view_valid = ReadSceneViewMatrix(g_state.armed_view);
@@ -1538,4 +1557,9 @@ FirstPersonAnchor MkwVRFirstPersonGetAnchor() noexcept {
return g_state.anchor;
}
HeldItem MkwVRFirstPersonGetHeldItem() noexcept {
std::lock_guard lock(g_mutex);
return g_state.held_item;
}
} // namespace mkw::vr
+8 -5
View File
@@ -656,12 +656,13 @@ void OpenXRInput::UpdateSimultaneousHandsAndControllers(bool wanted) {
"side to the cameras");
}
// Trackers only exist while tracked hands and hand steering are both on: the
// hands are only drawn while they can steer. They live as long as the session
// Tracked hands can carry a cockpit item even while stick steering is used.
// They live as long as the session
// otherwise (Idle and the cockpit's reset keep them); a runtime that refuses
// them is not asked again until the option is turned off and on.
void OpenXRInput::UpdateHandTrackers() {
const bool wanted = RuntimeConfigFile::VrHandTracking() && RuntimeConfigFile::VrHandSteering();
const bool wanted = RuntimeConfigFile::VrHandTracking() &&
(RuntimeConfigFile::VrHandSteering() || RuntimeConfigFile::VrCockpitItemHand() != "off");
UpdateSimultaneousHandsAndControllers(wanted);
if (!wanted) {
DestroyHandTrackers();
@@ -1417,8 +1418,10 @@ void OpenXRInput::UpdateDriving(XrTime display_time, const driving::SeatFrame& s
tracked = false;
}
DrivingHand& out = snapshot.hands[hand];
// Hands are shown only while they can steer.
out.tracked = hand_steering && (tracked || joints);
// The selected item hand is visible with stick steering too.
const auto item_hand = RuntimeConfigFile::VrCockpitItemHand();
const bool displays_item = item_hand == (hand == 0 ? "left" : "right");
out.tracked = (hand_steering || displays_item) && (tracked || joints);
out.held = false;
out.squeeze = squeeze;
out.seat_from_grip = seat_from_grip;
+4 -2
View File
@@ -1386,7 +1386,8 @@ private:
float lean_back_radians, AuroraCockpit& cockpit) noexcept {
cockpit.unitsPerMeter = units_per_meter;
const DrivingSnapshot driving = input_ != nullptr ? input_->Driving() : DrivingSnapshot{};
if (driving.hand_steering && !hand_meshes_loaded_ && runtime_ != nullptr) {
if ((driving.hand_steering || RuntimeConfigFile::VrCockpitItemHand() != "off") &&
!hand_meshes_loaded_ && runtime_ != nullptr) {
hand_meshes_loaded_ = true;
// Tracked hands' trackers, when they exist, serve the mesh too.
const XrHandTrackerEXT trackers[2]{input_ != nullptr ? input_->HandTracker(0) : XR_NULL_HANDLE,
@@ -1397,7 +1398,8 @@ private:
<< std::endl;
}
cockpit.active = driving.cockpit_active && position_valid && base_position_valid_ &&
(driving.synthetic_control || driving.hand_steering);
(driving.synthetic_control || driving.hand_steering ||
RuntimeConfigFile::VrCockpitItemHand() != "off");
if (!cockpit.active) {
return;
}
+7
View File
@@ -15,6 +15,13 @@ static RuntimeUserConfig Parse(const std::string& text) {
}
int main() {
for (std::string_view hand : {"left", "right", "off"}) {
Require(Parse("[vr]\ncockpit_item_hand = \"" + std::string(hand) + "\"\n")
.vrCockpitItemHand == std::string(hand));
}
Require(std::string_view(RuntimeConfigFile::kVrCockpitItemHandDefault) == "left");
Require(!Parse("[vr]\n").vrCockpitItemHand.has_value());
Require(!Parse("[vr]\ncockpit_item_hand = 1\n").vrCockpitItemHand.has_value());
// [vr] foveation: the Quest's foveated rendering level, index-matched to
// aurora_set_stereo_foveation.
for (std::string_view level : RuntimeConfigFile::kVrFoveationLevels) {
+84
View File
@@ -0,0 +1,84 @@
#include "vr/mkw_vr_item.h"
#include <cstdint>
#include <iostream>
#include <stdexcept>
#include <unordered_map>
namespace {
struct GuestMemory {
using AccessViolation = std::out_of_range;
inline static std::unordered_map<uint32_t, uint8_t> bytes;
inline static uint32_t fault = 0;
static bool Contains(uint32_t address, uint32_t length) {
for (uint32_t i = 0; i < length; ++i) if (!bytes.count(address + i)) return false;
return true;
}
static uint8_t Read8(uint32_t address) {
if (address == fault) throw AccessViolation("fault");
return bytes.at(address);
}
static uint32_t Read32(uint32_t address) {
uint32_t value = 0;
for (uint32_t i = 0; i < 4; ++i) value = (value << 8) | Read8(address + i);
return value;
}
static bool TryRead32(uint32_t address, uint32_t& out) {
try { out = Read32(address); return true; } catch (const AccessViolation&) { return false; }
}
static void Write32(uint32_t address, uint32_t value) {
for (uint32_t i = 0; i < 4; ++i) bytes[address + i] = uint8_t(value >> (24 - i * 8));
}
};
constexpr uint32_t manager = 0x81000000u, players = 0x82000000u;
constexpr uint32_t player = players + 7 * 0x248u;
int failures = 0;
void Check(bool value, const char* name) {
if (!value) { ++failures; std::cerr << "FAILED: " << name << '\n'; }
}
void Init() {
GuestMemory::bytes.clear(); GuestMemory::fault = 0;
GuestMemory::Write32(0x809C3618u, manager);
GuestMemory::Write32(manager + 0x14u, players);
for (uint32_t i = 0; i < 0x94u; ++i) GuestMemory::bytes[player + i] = 0;
GuestMemory::bytes[player + 0x18u] = 7;
GuestMemory::Write32(player + 0x90u, 1);
}
}
int main() {
using mkw::vr::detail::ReadHeldItem;
for (uint32_t id = 0; id <= 0x12u; ++id) {
Init(); GuestMemory::Write32(player + 0x8cu, id);
const auto item = ReadHeldItem<GuestMemory>(7, 19);
Check(item.valid && item.id == id && item.count == 1 && item.race_generation == 19,
"all 19 IDs, nonzero racer, generation");
}
Init(); GuestMemory::Write32(player + 0x8cu, 0x10); GuestMemory::Write32(player + 0x90u, 3);
Check(ReadHeldItem<GuestMemory>(7, 1).count == 3, "triple full");
GuestMemory::Write32(player + 0x90u, 2);
Check(ReadHeldItem<GuestMemory>(7, 1).count == 2, "triple remaining");
GuestMemory::Write32(player + 0x90u, 0);
Check(!ReadHeldItem<GuestMemory>(7, 1).valid, "inventory removed on use or damage");
GuestMemory::Write32(player + 0x90u, 1);
GuestMemory::Write32(player + 0x58u, 1);
Check(!ReadHeldItem<GuestMemory>(7, 1).valid, "roulette spinning");
GuestMemory::Write32(player + 0x58u, 0);
for (uint32_t id : {0x13u, 0x14u, 0xffu}) {
GuestMemory::Write32(player + 0x8cu, id);
Check(!ReadHeldItem<GuestMemory>(7, 1).valid, "empty or unsupported");
}
Init(); GuestMemory::Write32(player + 0x8cu, 0x0au);
Check(ReadHeldItem<GuestMemory>(7, 1).valid, "golden mushroom held");
GuestMemory::Write32(player + 0x90u, 0);
Check(!ReadHeldItem<GuestMemory>(7, 1).valid, "golden mushroom expiry");
Init(); GuestMemory::fault = player + 0x8cu;
const auto faulted = ReadHeldItem<GuestMemory>(7, 2);
Check(!faulted.valid && faulted.race_generation == 2, "read fault preserves generation");
Init(); GuestMemory::Write32(0x809C3618u, 0);
Check(!ReadHeldItem<GuestMemory>(7, 1).valid, "invalid manager");
Init(); GuestMemory::bytes[player + 0x18u] = 0;
Check(!ReadHeldItem<GuestMemory>(7, 1).valid, "racer identity mismatch");
return failures ? 1 : 0;
}