Fix model track-group movement and RGB destination-alpha blending
This commit is contained in:
@@ -0,0 +1,25 @@
|
||||
# Bear animation group binding
|
||||
|
||||
40250 `EterGrnLib/ModelInstanceUpdate.cpp::UpdateTransform` delegates to
|
||||
`GrannyUpdateModelMatrix`; that gameplay caller remains unchanged. ModelInstanceMotion
|
||||
binds controls through `GrannyPlayControlledAnimation`.
|
||||
|
||||
The real packed bear model is named Bip01. Both `bear/02.gr2` (walk) and `03.gr2`
|
||||
(run) contain Hedra first, Bip01 second. Hedra has zero accumulation; Bip01 has
|
||||
the translation in the corresponding unchanged MSA (-93.25 and -299.89 Y).
|
||||
The previous adapter exposed a count of two with one public group pointer, flattened
|
||||
bone binding across groups, and always accumulated the first group's translation.
|
||||
|
||||
The reader now preserves each group's name, placement, accumulation, text tracks,
|
||||
and flattened-track range. Runtime controls match the model name, excluding morph
|
||||
groups, and use only the selected group for bone binding and root accumulation.
|
||||
|
||||
`tests/port/port_granny_root_motion_test.cpp` loads those assets through PackBackend
|
||||
and the runtime APIs used by the live client. It checks both public groups, rotated
|
||||
root displacement across a loop boundary, inverse displacement, and stationary Hedra.
|
||||
The same test linked with HEAD's previous GrannyRuntime fails six assertions; the
|
||||
updated runtime passes. Desktop and `MT_TEST_MOBILE=1` login-flow regressions pass.
|
||||
Native macOS renderer and Android debug APK builds pass.
|
||||
|
||||
This is narrow evidence for constant-extracted motion. VDA extraction and complete
|
||||
Windows Granny output equivalence remain unverified; the aggregate contract stays PARTIAL.
|
||||
@@ -0,0 +1,36 @@
|
||||
# Assassin gungsin / 魅影 destination-alpha blending
|
||||
|
||||
The original skilldesc identifies 魅影 as skill 32, gungsin. The unchanged PC/PC2
|
||||
MSA files attach gungsintanyeong start/end/blow effects. Base and grade-four start/end
|
||||
MSE particles use SRCALPHA (5) / DESTALPHA (7), with MODULATE color operation.
|
||||
EffectInstance and ParticleSystemInstance match the reference except include casing;
|
||||
their state order, emission, asset colors, and textures remain unchanged.
|
||||
|
||||
RecordingDevice exposes an X8R8G8B8 back buffer and R5G6B5 GPU shadow targets.
|
||||
Both have implicit destination alpha 1. Vulkan stores the scene in RGBA and the
|
||||
previous pipeline incorrectly used the stored, draw-modified alpha for DESTALPHA.
|
||||
This attenuated the background, including zero-alpha opaque scene pixels.
|
||||
|
||||
`src/host/blend_state.h` normalizes DESTALPHA to ONE, INVDESTALPHA to ZERO, and
|
||||
SRCALPHASAT to ZERO for RGB targets before creating the Vulkan blend pipeline.
|
||||
Ordinary source-alpha, additive, and color blend factors remain unchanged. This
|
||||
helper applies only to the currently supported recorded RGB targets; future actual
|
||||
alpha render targets must retain their own destination-alpha semantics.
|
||||
|
||||
Focused CTest `host.blend_state` passes. The GPU regression command is:
|
||||
|
||||
```sh
|
||||
python3 tests/host/rgb_target_blend_test.py build/native/src/port/host_blend_state_test build/release/src/host/mt_native_render
|
||||
```
|
||||
|
||||
It draws an opaque zero-alpha background followed by a SRCALPHA/DESTALPHA overlay,
|
||||
reads the native renderer's BMP pixel, and checks RGB (128,128,160). The old native
|
||||
executable fails with (0,64,128); the updated executable passes. This exercises
|
||||
actual Vulkan blending, not just the mapping helper.
|
||||
|
||||
Native fake-server skill inspection used `--py-exec` to select the main instance,
|
||||
set assassin race 1 and armor 0, then `chr.PushOnceMotion(chr.MOTION_SKILL+2,0.0)`.
|
||||
Screenshots at 8 and 30 frames show bright particles and were inspected. These
|
||||
timed screenshots are not a deterministic Windows visual oracle. Android debug
|
||||
APK builds successfully; physical Android GPU and live-server skill-grade checks
|
||||
remain unverified. The aggregate render contract stays PARTIAL.
|
||||
@@ -642,3 +642,5 @@
|
||||
{"date": "2026-09-30", "unit": "UserInterface/UserInterface.cpp", "action": "port", "evidence": "ApplicationSetErrorString/ApplicationStringTable_Initialize verbatim + port.app_loop; Main ADAPTED to PythonBoot; Win32 entry/launcher options N_A"}
|
||||
{"date": "2026-09-30", "unit": "ScriptLib/PythonUtils.cpp", "action": "port", "evidence": "already verbatim (udiff: MSVC cast + perf counters); mapped"}
|
||||
{"date": "2026-09-30", "unit": "ScriptLib/Resource.cpp", "action": "port", "evidence": "verbatim 40250 (was a rewrite that also registered .png); ctest 29/29, screenshot unchanged"}
|
||||
{"date": "2026-09-30", "unit": "EterGrnLib/ModelInstanceUpdate.cpp", "action": "fix-platform-adapter", "evidence": "2026-09-30: native Granny adapter now selects the model-named track group, preserving all group metadata and limiting bone binding to that group. Real packed bear walk/run (auxiliary Hedra before Bip01) fail with the previous runtime and pass in tests/port/port_granny_root_motion_test.cpp. See audit/evidence/2026-09-30-bear-root-motion.md."}
|
||||
{"date": "2026-09-30", "unit": "EffectLib/ParticleSystemInstance.cpp", "action": "fix-platform-adapter", "evidence": "XRGB destination alpha semantics fixed in Vulkan pipeline; focused mapping + GPU pixel regression; original particle logic unchanged; desktop/mobile login regressions and Android build pass."}
|
||||
|
||||
+13
-4
@@ -1902,9 +1902,13 @@
|
||||
"project/mob_view_test.gd",
|
||||
"project/motion_event_payload_test.gd",
|
||||
"project/motion_registry_test.gd",
|
||||
"project/hit_view_test.gd"
|
||||
"project/hit_view_test.gd",
|
||||
"tests/port/port_granny_root_motion_test.cpp"
|
||||
],
|
||||
"last_test_result": "PASS: PC playersettingmodule motion registry (registration order/weights), HORSE parent fallback, knockdown/stand-up early-return, MSA MotionDuration authority, LoopData skill timing, motion event payloads, and hot clip/MSA cache reuse regressions passed; NPC/full RaceData/event-consumer parity remains PARTIAL"
|
||||
"last_test_result": "PASS: PC playersettingmodule motion registry (registration order/weights), HORSE parent fallback, knockdown/stand-up early-return, MSA MotionDuration authority, LoopData skill timing, motion event payloads, and hot clip/MSA cache reuse regressions passed; NPC/full RaceData/event-consumer parity remains PARTIAL",
|
||||
"commands": [
|
||||
"2026-09-30: ctest --test-dir build/native -R port.granny_root_motion --output-on-failure PASS; HEAD GrannyRuntime linked against the same packed-asset test FAIL (zero root displacement). audit/evidence/2026-09-30-bear-root-motion.md"
|
||||
]
|
||||
},
|
||||
"remaining": [
|
||||
"扩展已接入的 PC playersettingmodule registry 快照,对比 NPC motlist、horse/wedding/skill 的每个 motion key 和缺失 key 失败语义",
|
||||
@@ -3860,9 +3864,14 @@
|
||||
"project/motion_event_payload_test.gd",
|
||||
"project/motion_event_visibility_test.gd",
|
||||
"project/net_world_motion_event_test.gd",
|
||||
"project/motion_event_warp_test.gd"
|
||||
"project/motion_event_warp_test.gd",
|
||||
"tests/host/blend_state_test.cpp",
|
||||
"tests/host/rgb_target_blend_test.py"
|
||||
],
|
||||
"last_test_result": "PASS"
|
||||
"last_test_result": "PASS",
|
||||
"commands": [
|
||||
"2026-09-30: host.blend_state PASS; python3 tests/host/rgb_target_blend_test.py build/native/src/port/host_blend_state_test build/release/src/host/mt_native_render PASS (128,128,160); previous executable FAIL (0,64,128). Native gungsin motion screenshots inspected; Android APK build PASS. audit/evidence/2026-09-30-gungsin-blending.md"
|
||||
]
|
||||
},
|
||||
"remaining": [
|
||||
"type 4/6 的生产分支和专用字段消费;type 9 WARP 已接入但远端目标生命周期仍待 live 验证",
|
||||
|
||||
@@ -56,7 +56,8 @@
|
||||
"status": "PORTED",
|
||||
"impl": [
|
||||
"src/port/EffectLib/EffectInstance.cpp:CEffectInstance::OnRender"
|
||||
]
|
||||
],
|
||||
"note": "2026-09-30: reference statements remain verbatim apart from include case. Native Vulkan adapter now interprets destination alpha as 1 on recorded X8R8G8B8/R5G6B5 targets; gungsin start/end particles use SRCALPHA/DESTALPHA. Focused mapping and actual GPU pixel regression fail before/pass after. See audit/evidence/2026-09-30-gungsin-blending.md."
|
||||
},
|
||||
"CEffectInstance::SetGlobalMatrix": {
|
||||
"status": "PORTED",
|
||||
|
||||
@@ -44,7 +44,8 @@
|
||||
"status": "PORTED",
|
||||
"impl": [
|
||||
"src/port/EffectLib/ParticleSystemInstance.cpp:CParticleSystemInstance::OnRender"
|
||||
]
|
||||
],
|
||||
"note": "2026-09-30: reference statements remain verbatim apart from include case. Native Vulkan adapter now interprets destination alpha as 1 on recorded X8R8G8B8/R5G6B5 targets; gungsin start/end particles use SRCALPHA/DESTALPHA. Focused mapping and actual GPU pixel regression fail before/pass after. See audit/evidence/2026-09-30-gungsin-blending.md."
|
||||
},
|
||||
"CParticleSystemInstance::OnSetDataPointer": {
|
||||
"status": "PORTED",
|
||||
|
||||
@@ -20,7 +20,8 @@
|
||||
"status": "PORTED",
|
||||
"impl": [
|
||||
"src/port/EterGrnLib/ModelInstanceUpdate.cpp:CGrannyModelInstance::UpdateTransform"
|
||||
]
|
||||
],
|
||||
"note": "2026-09-30: native Granny adapter now selects the model-named track group, preserving all group metadata and limiting bone binding to that group. Real packed bear walk/run (auxiliary Hedra before Bip01) fail with the previous runtime and pass in tests/port/port_granny_root_motion_test.cpp. See audit/evidence/2026-09-30-bear-root-motion.md."
|
||||
},
|
||||
"CGrannyModelInstance::Deform": {
|
||||
"status": "PORTED",
|
||||
|
||||
@@ -125,6 +125,17 @@ struct BoneTrack {
|
||||
void sample(float t, Transform& out) const;
|
||||
};
|
||||
|
||||
// Preserve group boundaries: an animation can contain an auxiliary object group before its model.
|
||||
struct AnimationTrackGroup {
|
||||
std::string name;
|
||||
Transform initial_placement;
|
||||
int32_t accumulation_flags = 0;
|
||||
float loop_translation[3] = {0.0f, 0.0f, 0.0f};
|
||||
std::vector<TextTrack> text_tracks;
|
||||
size_t first_track = 0;
|
||||
size_t track_count = 0;
|
||||
};
|
||||
|
||||
struct Animation {
|
||||
std::string name;
|
||||
float duration = 0.0f;
|
||||
@@ -137,6 +148,7 @@ struct Animation {
|
||||
int32_t accumulation_flags = 0;
|
||||
float loop_translation[3] = {0.0f, 0.0f, 0.0f};
|
||||
std::vector<BoneTrack> tracks;
|
||||
std::vector<AnimationTrackGroup> track_groups;
|
||||
// t 处每个 track 的局部 4x4(granny_transform 组合语义,行主序)。
|
||||
// out 按 tracks 顺序;bone_index 见各 track。
|
||||
void sample_local(float t, std::vector<Mat4>& out) const;
|
||||
|
||||
+47
-37
@@ -110,47 +110,53 @@ bool File::Impl::extract_animation(Ref anim_obj, Ref anim_type, const Skeleton*
|
||||
out.track_group_count = int32_t(groups.size());
|
||||
const auto& tgms = parse_type(mTG->ref_type);
|
||||
|
||||
// TrackGroups[0].AccumulationFlags (Int32) + LoopTranslation (Real32[3])
|
||||
if (!groups.empty() && groups[0].valid()) {
|
||||
if (const TypeMember* m = find_member(tgms, "AccumulationFlags"))
|
||||
out.accumulation_flags = rd_i32(member_slot(groups[0], *m));
|
||||
if (const TypeMember* m = find_member(tgms, "LoopTranslation")) {
|
||||
Ref r = member_slot(groups[0], *m);
|
||||
for (int i = 0; i < 3; ++i)
|
||||
out.loop_translation[i] = rd_f32(Ref{r.section, r.offset + uint32_t(i) * 4u});
|
||||
}
|
||||
}
|
||||
|
||||
// TrackGroups[0].TextTracks: RefToArray text_track{String Name; RefToArray Entries(
|
||||
// text_track_entry{Real32 TimeStamp; String Text})}(motion event 用)
|
||||
if (!groups.empty() && groups[0].valid())
|
||||
if (const TypeMember* mTx = find_member(tgms, "TextTracks")) {
|
||||
Ref xb; uint32_t xc = 0, xs = 0;
|
||||
if (read_ref_to_array(groups[0], *mTx, xb, xc, xs) && xc && xs) {
|
||||
const auto& xms = parse_type(mTx->ref_type);
|
||||
const TypeMember* mXN = find_member(xms, "Name");
|
||||
const TypeMember* mXE = find_member(xms, "Entries");
|
||||
for (uint32_t i = 0; i < xc && i < 4096; ++i) {
|
||||
Ref x = {xb.section, xb.offset + i * xs};
|
||||
TextTrack tt;
|
||||
if (mXN) tt.name = rd_str(member_slot(x, *mXN));
|
||||
Ref eb; uint32_t ec = 0, es = 0;
|
||||
if (mXE && read_ref_to_array(x, *mXE, eb, ec, es) && ec && es) {
|
||||
const auto& ems = parse_type(mXE->ref_type);
|
||||
const TypeMember* mTS = find_member(ems, "TimeStamp");
|
||||
const TypeMember* mTT = find_member(ems, "Text");
|
||||
for (uint32_t k = 0; k < ec && k < 65536; ++k) {
|
||||
Ref e = {eb.section, eb.offset + k * es};
|
||||
TextTrackEntry te;
|
||||
if (mTS) te.time_stamp = rd_f32(member_slot(e, *mTS));
|
||||
if (mTT) te.text = rd_str(member_slot(e, *mTT));
|
||||
tt.entries.push_back(std::move(te));
|
||||
for (Ref group : groups) {
|
||||
AnimationTrackGroup data;
|
||||
if (group.valid()) {
|
||||
if (const auto* m = find_member(tgms, "Name")) data.name = rd_str(member_slot(group, *m));
|
||||
if (const auto* m = find_member(tgms, "InitialPlacement")) read_transform(member_slot(group, *m), data.initial_placement);
|
||||
if (const auto* m = find_member(tgms, "AccumulationFlags")) data.accumulation_flags = rd_i32(member_slot(group, *m));
|
||||
if (const auto* m = find_member(tgms, "LoopTranslation")) {
|
||||
Ref r = member_slot(group, *m);
|
||||
for (int i = 0; i < 3; ++i) data.loop_translation[i] = rd_f32({r.section, r.offset + uint32_t(i) * 4u});
|
||||
}
|
||||
if (const auto* mTx = find_member(tgms, "TextTracks")) {
|
||||
Ref xb; uint32_t xc = 0, xs = 0;
|
||||
if (read_ref_to_array(group, *mTx, xb, xc, xs) && xc && xs) {
|
||||
const auto& xms = parse_type(mTx->ref_type);
|
||||
const auto* mXN = find_member(xms, "Name");
|
||||
const auto* mXE = find_member(xms, "Entries");
|
||||
for (uint32_t i = 0; i < xc && i < 4096; ++i) {
|
||||
Ref x = {xb.section, xb.offset + i * xs};
|
||||
TextTrack tt;
|
||||
if (mXN) tt.name = rd_str(member_slot(x, *mXN));
|
||||
Ref eb; uint32_t ec = 0, es = 0;
|
||||
if (mXE && read_ref_to_array(x, *mXE, eb, ec, es) && ec && es) {
|
||||
const auto& ems = parse_type(mXE->ref_type);
|
||||
const auto* mTS = find_member(ems, "TimeStamp");
|
||||
const auto* mTT = find_member(ems, "Text");
|
||||
for (uint32_t k = 0; k < ec && k < 65536; ++k) {
|
||||
Ref e = {eb.section, eb.offset + k * es};
|
||||
TextTrackEntry te;
|
||||
if (mTS) te.time_stamp = rd_f32(member_slot(e, *mTS));
|
||||
if (mTT) te.text = rd_str(member_slot(e, *mTT));
|
||||
tt.entries.push_back(std::move(te));
|
||||
}
|
||||
}
|
||||
data.text_tracks.push_back(std::move(tt));
|
||||
}
|
||||
out.text_tracks.push_back(std::move(tt));
|
||||
}
|
||||
}
|
||||
}
|
||||
out.track_groups.push_back(std::move(data));
|
||||
}
|
||||
// Existing inspection API exposes the first group; runtime binding uses track_groups by name.
|
||||
if (!out.track_groups.empty()) {
|
||||
const auto& first = out.track_groups.front();
|
||||
out.accumulation_flags = first.accumulation_flags;
|
||||
std::copy_n(first.loop_translation, 3, out.loop_translation);
|
||||
out.text_tracks = first.text_tracks;
|
||||
}
|
||||
const TypeMember* mTT = find_member(tgms, "TransformTracks");
|
||||
if (!mTT) return true;
|
||||
|
||||
@@ -167,7 +173,10 @@ bool File::Impl::extract_animation(Ref anim_obj, Ref anim_type, const Skeleton*
|
||||
const TypeMember* mPos = find_member(ttms, "PositionCurve");
|
||||
const TypeMember* mScl = find_member(ttms, "ScaleShearCurve");
|
||||
|
||||
for (Ref g : groups) {
|
||||
for (size_t group_index = 0; group_index < groups.size(); ++group_index) {
|
||||
Ref g = groups[group_index];
|
||||
auto& group_data = out.track_groups[group_index];
|
||||
group_data.first_track = out.tracks.size();
|
||||
if (!g.valid()) continue;
|
||||
Ref base; uint32_t count = 0, stride = 0;
|
||||
if (!read_ref_to_array(g, *mTT, base, count, stride) || count == 0) continue;
|
||||
@@ -191,6 +200,7 @@ bool File::Impl::extract_animation(Ref anim_obj, Ref anim_type, const Skeleton*
|
||||
if (skel->bones[k].name == bt.bone_name) { bt.bone_index = int32_t(k); break; }
|
||||
out.tracks.push_back(std::move(bt));
|
||||
}
|
||||
group_data.track_count = out.tracks.size() - group_data.first_track;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,16 @@
|
||||
#pragma once
|
||||
#include <cstdint>
|
||||
|
||||
namespace mt_host {
|
||||
// PORT: the recorded back buffer is X8R8G8B8 and GPU shadow targets are R5G6B5.
|
||||
// Their destination alpha is always 1, even when Vulkan storage has an alpha
|
||||
// channel. Reading that storage alpha makes DESTALPHA particles darken the scene.
|
||||
inline std::uint8_t d3d_blend_for_rgb_target(std::uint8_t factor) {
|
||||
switch (factor) {
|
||||
case 7: return 2; // DESTALPHA -> ONE
|
||||
case 8: return 1; // INVDESTALPHA -> ZERO
|
||||
case 11: return 1; // SRCALPHASAT -> min(As, 1 - Ad) = ZERO (RGB)
|
||||
default: return factor;
|
||||
}
|
||||
}
|
||||
} // namespace mt_host
|
||||
@@ -2,6 +2,7 @@
|
||||
#include "vulkan_window.h"
|
||||
|
||||
#include "host_util.h"
|
||||
#include "blend_state.h"
|
||||
|
||||
#include <cstring>
|
||||
#include <stdexcept>
|
||||
@@ -212,7 +213,7 @@ VkPipeline VulkanWindow::get_pipeline(std::uint8_t cull, std::uint8_t depth, std
|
||||
}
|
||||
|
||||
auto d3d_to_vk_blend = [](std::uint8_t d3d, VkBlendFactor fallback) -> VkBlendFactor {
|
||||
switch (d3d) {
|
||||
switch (d3d_blend_for_rgb_target(d3d)) {
|
||||
case 1: return VK_BLEND_FACTOR_ZERO;
|
||||
case 2: return VK_BLEND_FACTOR_ONE;
|
||||
case 3: return VK_BLEND_FACTOR_SRC_COLOR;
|
||||
|
||||
@@ -285,14 +285,19 @@ struct ModelRec
|
||||
std::vector<granny_model_mesh_binding> mesh_bindings;
|
||||
};
|
||||
|
||||
struct TrackGroupRec
|
||||
{
|
||||
granny_track_group pub{};
|
||||
std::vector<granny_text_track> text_tracks;
|
||||
std::vector<std::vector<granny_text_track_entry>> entries;
|
||||
};
|
||||
|
||||
struct AnimationRec
|
||||
{
|
||||
granny_animation pub{};
|
||||
const gr2::Animation* src = nullptr;
|
||||
granny_track_group group{};
|
||||
granny_track_group* groups[1]{};
|
||||
std::vector<granny_text_track> text_tracks;
|
||||
std::vector<std::vector<granny_text_track_entry>> entries;
|
||||
std::vector<TrackGroupRec> group_records;
|
||||
std::vector<granny_track_group*> groups;
|
||||
};
|
||||
|
||||
const MeshRec* mesh_rec(const granny_mesh* m)
|
||||
@@ -481,23 +486,30 @@ void build_file(granny_file& f)
|
||||
r.pub.Duration = src.duration;
|
||||
r.pub.TimeStep = src.time_step;
|
||||
r.pub.Oversampling = src.oversampling;
|
||||
r.entries.resize(src.text_tracks.size());
|
||||
for (size_t t = 0; t < src.text_tracks.size(); ++t)
|
||||
{
|
||||
for (const gr2::TextTrackEntry& e : src.text_tracks[t].entries)
|
||||
r.entries[t].push_back({e.time_stamp, e.text.c_str()});
|
||||
granny_text_track tt{};
|
||||
tt.Name = src.text_tracks[t].name.c_str();
|
||||
tt.EntryCount = int32_t(r.entries[t].size());
|
||||
tt.Entries = r.entries[t].empty() ? nullptr : r.entries[t].data();
|
||||
r.text_tracks.push_back(tt);
|
||||
}
|
||||
r.group.Name = r.pub.Name;
|
||||
r.group.TextTrackCount = int32_t(r.text_tracks.size());
|
||||
r.group.TextTracks = r.text_tracks.empty() ? nullptr : r.text_tracks.data();
|
||||
r.groups[0] = &r.group;
|
||||
r.pub.TrackGroupCount = std::max<int32_t>(1, src.track_group_count);
|
||||
r.pub.TrackGroups = r.groups;
|
||||
r.group_records.resize(src.track_groups.size());
|
||||
r.groups.resize(src.track_groups.size());
|
||||
for (size_t g = 0; g < src.track_groups.size(); ++g) {
|
||||
const auto& data = src.track_groups[g];
|
||||
auto& record = r.group_records[g];
|
||||
record.entries.resize(data.text_tracks.size());
|
||||
for (size_t t = 0; t < data.text_tracks.size(); ++t) {
|
||||
for (const auto& e : data.text_tracks[t].entries)
|
||||
record.entries[t].push_back({e.time_stamp, e.text.c_str()});
|
||||
granny_text_track tt{};
|
||||
tt.Name = data.text_tracks[t].name.c_str();
|
||||
tt.EntryCount = int32_t(record.entries[t].size());
|
||||
tt.Entries = record.entries[t].empty() ? nullptr : record.entries[t].data();
|
||||
record.text_tracks.push_back(tt);
|
||||
}
|
||||
record.pub.Name = data.name.c_str();
|
||||
record.pub.Flags = data.accumulation_flags;
|
||||
to_granny(data.initial_placement, record.pub.InitialPlacement);
|
||||
record.pub.TextTrackCount = int32_t(record.text_tracks.size());
|
||||
record.pub.TextTracks = record.text_tracks.empty() ? nullptr : record.text_tracks.data();
|
||||
r.groups[g] = &record.pub;
|
||||
}
|
||||
r.pub.TrackGroupCount = int32_t(r.groups.size());
|
||||
r.pub.TrackGroups = r.groups.data();
|
||||
f.animation_ptrs.push_back(&r.pub);
|
||||
}
|
||||
|
||||
@@ -1045,6 +1057,7 @@ struct granny_control
|
||||
{
|
||||
granny_model_instance* instance = nullptr;
|
||||
const AnimationRec* animation = nullptr;
|
||||
const gr2::AnimationTrackGroup* track_group = nullptr;
|
||||
std::vector<int> bone_track; // model bone → animation track (-1: none)
|
||||
bool live = false;
|
||||
float current_clock = 0.0f;
|
||||
@@ -1172,8 +1185,8 @@ void GrannyFreeCompletedModelControls(granny_model_instance const* ModelInstance
|
||||
release_control(c);
|
||||
}
|
||||
|
||||
// PORT: GrannyUpdateModelMatrix reimplemented for constant-extraction root motion only (walk/run carry
|
||||
// AccumulationFlags=AccumulationExtracted + LoopTranslation, e.g. warrior run (0,-300,0) = .msa Accumulation).
|
||||
// PORT: constant-extraction root motion comes from the group bound to the model by name,
|
||||
// not necessarily TrackGroups[0] (bear: Hedra decoration then Bip01, with translation only in Bip01).
|
||||
// Each live control contributes LoopTranslation over the local-clock span of the last SecondsElapsed,
|
||||
// blended by control weight; Dest = Delta x ModelMatrix (row vectors). VDA extraction is not implemented.
|
||||
void GrannyUpdateModelMatrix(granny_model_instance const* ModelInstance, granny_real32 SecondsElapsed,
|
||||
@@ -1188,8 +1201,8 @@ void GrannyUpdateModelMatrix(granny_model_instance const* ModelInstance, granny_
|
||||
for (const granny_control* c : ModelInstance->controls)
|
||||
{
|
||||
const float w = control_weight(c);
|
||||
const gr2::Animation* a = c->animation->src;
|
||||
if (w <= 0.0f || !a || !(a->accumulation_flags & 1) || (a->accumulation_flags & 4))
|
||||
const auto* group = c->track_group;
|
||||
if (w <= 0.0f || !group || !(group->accumulation_flags & 1) || (group->accumulation_flags & 4))
|
||||
continue;
|
||||
const float dur = c->animation->pub.Duration;
|
||||
float t1 = c->local_clock, t0 = t1 - SecondsElapsed * c->speed;
|
||||
@@ -1201,7 +1214,7 @@ void GrannyUpdateModelMatrix(granny_model_instance const* ModelInstance, granny_
|
||||
}
|
||||
const float k = dur > 0.0f ? (t1 - t0) / dur : 0.0f;
|
||||
for (int i = 0; i < 3; ++i)
|
||||
delta[i] += w * k * a->loop_translation[i];
|
||||
delta[i] += w * k * group->loop_translation[i];
|
||||
total += w;
|
||||
}
|
||||
if (total > 0.0f)
|
||||
@@ -1218,6 +1231,14 @@ granny_control* GrannyPlayControlledAnimation(granny_real32 StartTime, granny_an
|
||||
{
|
||||
if (!Animation || !Model)
|
||||
return nullptr;
|
||||
const auto& source = *animation_rec(Animation)->src;
|
||||
const gr2::AnimationTrackGroup* group = nullptr;
|
||||
for (const auto& candidate : source.track_groups)
|
||||
if (!(candidate.accumulation_flags & 8) && name_equal(candidate.name.c_str(), Model->model->Name)) {
|
||||
group = &candidate;
|
||||
break;
|
||||
}
|
||||
if (!group) return nullptr;
|
||||
granny_control* c;
|
||||
auto& pool = control_pool();
|
||||
if (pool.empty())
|
||||
@@ -1230,6 +1251,7 @@ granny_control* GrannyPlayControlledAnimation(granny_real32 StartTime, granny_an
|
||||
}
|
||||
c->instance = Model;
|
||||
c->animation = animation_rec(Animation);
|
||||
c->track_group = group;
|
||||
c->live = true;
|
||||
c->current_clock = StartTime;
|
||||
|
||||
@@ -1237,7 +1259,7 @@ granny_control* GrannyPlayControlledAnimation(granny_real32 StartTime, granny_an
|
||||
const auto& tracks = c->animation->src->tracks;
|
||||
c->bone_track.assign(sk ? sk->BoneCount : 0, -1);
|
||||
for (int b = 0; b < int(c->bone_track.size()); ++b)
|
||||
for (size_t t = 0; t < tracks.size(); ++t)
|
||||
for (size_t t = group->first_track; t < group->first_track + group->track_count; ++t)
|
||||
if (name_equal(tracks[t].bone_name.c_str(), sk->Bones[b].Name))
|
||||
{
|
||||
c->bone_track[b] = int(t);
|
||||
|
||||
@@ -283,6 +283,14 @@ if(BUILD_TESTING AND CMAKE_SYSTEM_NAME STREQUAL CMAKE_HOST_SYSTEM_NAME)
|
||||
COMMAND $<TARGET_FILE:port_app_loop_test> ${MT_40250_CLIENT} ${MT_PYTHON_STDLIB_ZIP})
|
||||
set_tests_properties(port.app_loop PROPERTIES SKIP_RETURN_CODE 77)
|
||||
|
||||
add_executable(host_blend_state_test "${PROJECT_SOURCE_DIR}/tests/host/blend_state_test.cpp")
|
||||
add_test(NAME host.blend_state COMMAND host_blend_state_test)
|
||||
|
||||
add_executable(port_granny_root_motion_test ${PROJECT_SOURCE_DIR}/tests/port/port_granny_root_motion_test.cpp)
|
||||
target_link_libraries(port_granny_root_motion_test PRIVATE port_platform)
|
||||
add_test(NAME port.granny_root_motion COMMAND port_granny_root_motion_test "${MT_40250_CLIENT}")
|
||||
set_tests_properties(port.granny_root_motion PROPERTIES SKIP_RETURN_CODE 77)
|
||||
|
||||
add_executable(port_login_flow_test ${PROJECT_SOURCE_DIR}/tests/port/port_login_flow_test.cpp
|
||||
${PROJECT_SOURCE_DIR}/tests/port/port_login_flow_server.cpp
|
||||
${PROJECT_SOURCE_DIR}/tests/port/classic/classic_cipher.cpp)
|
||||
|
||||
@@ -0,0 +1,36 @@
|
||||
#include "../../src/host/blend_state.h"
|
||||
#include "../../src/host/draw_capture.h"
|
||||
#include <cstdio>
|
||||
|
||||
int main(int argc, char** argv) {
|
||||
int failures = 0;
|
||||
auto check = [&](bool ok, const char* message) {
|
||||
if (!ok) { std::fprintf(stderr, "FAIL %s\n", message); ++failures; }
|
||||
};
|
||||
using mt_host::d3d_blend_for_rgb_target;
|
||||
check(d3d_blend_for_rgb_target(7) == 2, "gungsin DESTALPHA preserves the background on XRGB");
|
||||
check(d3d_blend_for_rgb_target(8) == 1, "INVDESTALPHA contributes zero on XRGB");
|
||||
check(d3d_blend_for_rgb_target(11) == 1, "SRCALPHASAT contributes zero RGB on XRGB");
|
||||
for (std::uint8_t factor = 1; factor <= 10; ++factor)
|
||||
if (factor != 7 && factor != 8)
|
||||
check(d3d_blend_for_rgb_target(factor) == factor, "ordinary alpha/additive/color factors preserved");
|
||||
if (argc == 2) {
|
||||
std::vector<Render3DDraw> draws;
|
||||
// An opaque scene draw can output alpha 0 into Vulkan's RGBA storage.
|
||||
// Gungsin's SRCALPHA / DESTALPHA must still preserve that XRGB background.
|
||||
for (auto color : {0x00804020u, 0x800080ffu}) {
|
||||
Render3DDraw d{};
|
||||
for (auto* matrix : {d.world, d.view, d.proj})
|
||||
for (int i = 0; i < 16; ++i) matrix[i] = (i % 5 == 0) ? 1 : 0;
|
||||
d.positions = {-1,-1,0.5f, 1,-1,0.5f, 1,1,0.5f, -1,1,0.5f};
|
||||
d.indices = {0,1,2,0,2,3};
|
||||
d.diffuse = {color,color,color,color};
|
||||
d.color_op[0] = d.alpha_op[0] = 2; // SELECTARG1 / DIFFUSE
|
||||
d.color_op[1] = d.alpha_op[1] = 1; // DISABLE
|
||||
if (!draws.empty()) { d.alpha_blend = 1; d.src_blend = 5; d.dest_blend = 7; }
|
||||
draws.push_back(std::move(d));
|
||||
}
|
||||
native_draw_capture::write(argv[1], draws, {}, 960, 640, {});
|
||||
}
|
||||
return failures ? 1 : 0;
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
"""GPU regression: python3 rgb_target_blend_test.py <host_blend_state_test> <mt_native_render>."""
|
||||
import pathlib
|
||||
import struct
|
||||
import subprocess
|
||||
import sys
|
||||
import tempfile
|
||||
|
||||
with tempfile.TemporaryDirectory(prefix="metin2-rgb-blend-") as directory:
|
||||
capture = pathlib.Path(directory) / "destalpha.capture"
|
||||
screenshot = pathlib.Path(directory) / "destalpha.bmp"
|
||||
subprocess.run([sys.argv[1], str(capture)], check=True)
|
||||
subprocess.run([sys.argv[2], "--capture", str(capture), "--frames", "1",
|
||||
"--width", "320", "--height", "240", "--screenshot-out", str(screenshot)],
|
||||
check=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
|
||||
data = screenshot.read_bytes()
|
||||
offset = struct.unpack_from("<I", data, 10)[0]
|
||||
width, height = struct.unpack_from("<ii", data, 18)
|
||||
bits = struct.unpack_from("<H", data, 28)[0]
|
||||
assert bits in (24, 32), f"unsupported screenshot format: {bits}"
|
||||
stride = ((width * bits + 31) // 32) * 4
|
||||
pixel = offset + (abs(height) // 2) * stride + (width // 2) * (bits // 8)
|
||||
actual = tuple(data[pixel:pixel + 3][::-1])
|
||||
expected = (128, 128, 160) # source * 128/255 + background * 1
|
||||
assert all(abs(a - b) <= 1 for a, b in zip(actual, expected)), (actual, expected)
|
||||
print(f"PASS XRGB SRCALPHA/DESTALPHA GPU pixel: {actual}")
|
||||
@@ -0,0 +1,90 @@
|
||||
// Real packed bear assets: the auxiliary Hedra group precedes the moving Bip01 group.
|
||||
#include "granny.h"
|
||||
#include "../../src/platform/PackBackend.h"
|
||||
#include <cmath>
|
||||
#include <cstdio>
|
||||
#include <cstdlib>
|
||||
#include <cstring>
|
||||
#include <sstream>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
namespace {
|
||||
int failures = 0;
|
||||
void check(bool ok, const char* message) {
|
||||
if (!ok) { std::fprintf(stderr, "FAIL %s\n", message); ++failures; }
|
||||
}
|
||||
granny_file* load(const std::string& path) {
|
||||
std::vector<uint8_t> bytes;
|
||||
if (!mtpack40250::read(path, bytes)) return nullptr;
|
||||
return GrannyReadEntireFileFromMemory(bytes.size(), bytes.data());
|
||||
}
|
||||
void test_motion(granny_model* model, const char* name) {
|
||||
const std::string base = "d:/ymir work/monster/bear/";
|
||||
granny_file* file = load(base + name + ".gr2");
|
||||
check(file != nullptr, "bear animation loads from the live pack path");
|
||||
if (!file) return;
|
||||
auto* info = GrannyGetFileInfo(file);
|
||||
check(info && info->AnimationCount == 1, "one animation");
|
||||
if (!info || info->AnimationCount != 1) { GrannyFreeFile(file); return; }
|
||||
auto* animation = info->Animations[0];
|
||||
check(animation->TrackGroupCount == 2, "both track groups exposed safely");
|
||||
const bool auxiliaryFirst = animation->TrackGroupCount > 0 &&
|
||||
!std::strcmp(animation->TrackGroups[0]->Name, "Hedra");
|
||||
check(auxiliaryFirst, "auxiliary group first");
|
||||
if (animation->TrackGroupCount == 2 && auxiliaryFirst) {
|
||||
check(!std::strcmp(animation->TrackGroups[1]->Name, "Bip01"), "model group second");
|
||||
}
|
||||
std::vector<uint8_t> msa;
|
||||
check(mtpack40250::read(base + name + ".msa", msa), "motion specification loads");
|
||||
float accumulation[3] = {};
|
||||
std::istringstream lines(std::string(msa.begin(), msa.end()));
|
||||
for (std::string line; std::getline(lines, line);)
|
||||
if (line.rfind("Accumulation", 0) == 0)
|
||||
std::sscanf(line.c_str(), "Accumulation %f %f %f", accumulation, accumulation + 1, accumulation + 2);
|
||||
check(accumulation[1] < 0, "moving resource has forward accumulation");
|
||||
for (bool inverse : {false, true}) {
|
||||
auto* instance = GrannyInstantiateModel(model);
|
||||
auto* control = GrannyPlayControlledAnimation(0, animation, instance);
|
||||
check(control != nullptr, "animation binds to named model");
|
||||
if (control) {
|
||||
GrannySetControlLoopCount(control, 0);
|
||||
const float dt = 0.1f;
|
||||
const float clock = animation->Duration + dt; // across the loop boundary
|
||||
GrannySetModelClock(instance, clock);
|
||||
float matrix[16] = {0,1,0,0, -1,0,0,0, 0,0,1,0, 100,200,0,1};
|
||||
GrannyUpdateModelMatrix(instance, dt, matrix, matrix, inverse);
|
||||
const float expectedX = 100 - (inverse ? -1.0f : 1.0f) * accumulation[1] * dt / animation->Duration;
|
||||
check(std::abs(matrix[12] - expectedX) < 0.02f && std::abs(matrix[13] - 200) < 0.02f,
|
||||
"model group root displacement, loop, rotation and inverse");
|
||||
}
|
||||
GrannyFreeModelInstance(instance);
|
||||
}
|
||||
// Hedra has no root displacement even though the other group moves.
|
||||
granny_model decoration = *model;
|
||||
decoration.Name = "Hedra";
|
||||
auto* instance = GrannyInstantiateModel(&decoration);
|
||||
auto* control = GrannyPlayControlledAnimation(0, animation, instance);
|
||||
check(control != nullptr, "auxiliary object binds to its own group");
|
||||
GrannySetModelClock(instance, 0.1f);
|
||||
float matrix[16] = {1,0,0,0, 0,1,0,0, 0,0,1,0, 0,0,0,1};
|
||||
GrannyUpdateModelMatrix(instance, 0.1f, matrix, matrix, false);
|
||||
check(matrix[12] == 0 && matrix[13] == 0, "auxiliary group does not inherit bear movement");
|
||||
GrannyFreeModelInstance(instance);
|
||||
GrannyFreeFile(file);
|
||||
}
|
||||
}
|
||||
int main(int argc, char** argv) {
|
||||
if (argc < 2 || !mtpack40250::initialize(argv[1])) return std::getenv("MT_ASSETS_STRICT") ? 1 : 77;
|
||||
auto* file = load("d:/ymir work/monster/bear/bear.gr2");
|
||||
check(file != nullptr, "bear model loads");
|
||||
if (!file) return 1;
|
||||
auto* info = GrannyGetFileInfo(file);
|
||||
check(info && info->ModelCount == 1, "one bear model");
|
||||
if (info && info->ModelCount == 1) {
|
||||
test_motion(info->Models[0], "02");
|
||||
test_motion(info->Models[0], "03");
|
||||
}
|
||||
GrannyFreeFile(file);
|
||||
return failures ? 1 : 0;
|
||||
}
|
||||
Reference in New Issue
Block a user