Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions port/include/stubs/editloop.hpp
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
#pragma once

#include <mg_memory.hpp>

// Expose the retail town ownership state without changing its logic or layout.
extern int LoopMode asm("EditLoop_Mode");
extern int ControlMode asm("EditLoop_Control");
Expand All @@ -14,3 +16,7 @@ extern mgCFrame *BlueBicMark asm("EditLoop_BlueBicMark");

void PortRetailUpdateTrBoxFlag(int map_no) asm("EditLoop_UpdateTrBoxFlag");
int PortRetailEditDraw() asm("EditLoop_EditDraw");

// SetDataPacket reserves the controlled character's memory again while the character stays loaded there
// (port/src/mg_memory.cpp).
u_long128 *MemoryAllocKept(mgCMemory &memory, int size);
3 changes: 3 additions & 0 deletions port/src/arena_root.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
#include <print>
#include <vector>

#include "character_motion.hpp"
#include "platform/arena.hpp"
#include "platform/memory.hpp"

Expand Down Expand Up @@ -34,6 +35,8 @@ u_long128 *ArenaRootFromBuffer(const u_long128 *buffer, int quadwords, const cha
// After the first import the game owns the mapped contents. Rebinding a cursor to the
// same root must not replace live records with the original static buffer's bytes.
if (std::ranges::find(imported, base) == imported.end()) {
// The root may already be mapped for the same key, so the copy can land on registered motion storage.
character_motion::ForgetStorage(base, base + quadwords);
std::memcpy(base, buffer, static_cast<std::size_t>(quadwords) * sizeof(u_long128));
imported.push_back(base);
}
Expand Down
110 changes: 94 additions & 16 deletions port/src/character_motion.cpp
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
#include "common.h"

#include <algorithm>
#include <cstdint>
#include <cstring>
#include <optional>
#include <span>
#include <unordered_map>
Expand All @@ -12,19 +14,29 @@

namespace character_motion {
namespace {
/**
* The registered actions of one motion set.
*/
struct Set {
std::uint32_t stamp = 0; /**< Registration stamp, also written into the anchor. */
std::vector<Action> actions; /**< Actions in game-frame order. */
std::span<const std::byte> storage; /**< Arena memory holding the channels and the anchor. */
};

/**
* The action last posed on one frame tree, the source of the next blend.
*/
struct Posed {
const Mot_List *anchor = nullptr; /**< Motion set of the action. */
std::uint32_t stamp = 0; /**< Registration of the motion set when posed. */
std::size_t action = 0; /**< Index of the action in its motion set. */
};

/**
* Registered actions by the channel anchor of their motion set.
* Registered motion sets by their channel anchor.
*/
std::unordered_map<const Mot_List *, std::vector<Action> > &Registry() {
static std::unordered_map<const Mot_List *, std::vector<Action> > registry;
std::unordered_map<const Mot_List *, Set> &Registry() {
static std::unordered_map<const Mot_List *, Set> registry;
return registry;
}

Expand All @@ -36,6 +48,36 @@ std::unordered_map<const mgCFrame *, Posed> &PosedActions() {
return posed;
}

/**
* Reads the registration stamp an anchor carries in its otherwise unused tail.
*/
std::uint32_t StampOf(const Mot_List &anchor) {
std::uint32_t stamp;
std::memcpy(&stamp, anchor.unk_1C, sizeof(stamp));
return stamp;
}

/**
* Finds the registered set of an anchor.
*
* A set whose storage an arena handed out again is already forgotten. An anchor overwritten some other
* way no longer carries its stamp, so its set is forgotten here.
*/
const Set *Lookup(const Mot_List *anchor) {
if (!anchor) {
return nullptr;
}
const auto found = Registry().find(anchor);
if (found == Registry().end()) {
return nullptr;
}
if (StampOf(*anchor) != found->second.stamp) {
Forget(anchor);
return nullptr;
}
return &found->second;
}

/**
* Finds the index of the action whose interval holds a game frame.
*/
Expand Down Expand Up @@ -110,26 +152,55 @@ void Apply(mgCFrame &root, const Action &action, float local, mgCCamera *camera)
}
} // namespace

void Register(const Mot_List *anchor, std::vector<Action> actions) {
Registry().insert_or_assign(anchor, std::move(actions));
void Register(Mot_List *anchor, std::vector<Action> actions, std::span<const std::byte> storage) {
static std::uint32_t next_stamp = 0;
// A cleared arena holds zero, so zero never stamps a set.
if (++next_stamp == 0) {
++next_stamp;
}
// A set registered again at the same anchor drops the old one's poses along with its actions.
Forget(anchor);
std::memcpy(anchor->unk_1C, &next_stamp, sizeof(next_stamp));
Registry().insert_or_assign(anchor, Set{.stamp = next_stamp, .actions = std::move(actions), .storage = storage});
}

void Forget(const Mot_List *anchor) {
Registry().erase(anchor);
std::erase_if(PosedActions(), [anchor](const auto &entry) { return entry.second.anchor == anchor; });
}

void ForgetStorage(const void *begin, const void *end) {
if (Registry().empty()) {
return;
}
const auto *first = static_cast<const std::byte *>(begin);
const auto *last = static_cast<const std::byte *>(end);
std::vector<const Mot_List *> released;
for (const auto &[anchor, set] : Registry()) {
if (set.storage.data() < last && first < set.storage.data() + set.storage.size()) {
released.push_back(anchor);
}
}
for (const auto *anchor : released) {
Forget(anchor);
}
}

void ForgetFrame(const mgCFrame *root) {
PosedActions().erase(root);
}

void Clear() {
Registry().clear();
PosedActions().clear();
}

std::span<const Action> Actions(const tagMOTION_TYPE &motion) {
const auto found = Registry().find(motion.motion_list);
if (found == Registry().end()) {
const auto *set = Lookup(motion.motion_list);
if (!set) {
return {};
}
return found->second;
return set->actions;
}

const Action *Find(const tagMOTION_TYPE &motion, std::string_view name) {
Expand All @@ -145,14 +216,17 @@ const Action *At(const tagMOTION_TYPE &motion, float frame) {
}

void Pose(mgCFrame &root, tagMOTION_TYPE &motion, float frame, mgCCamera *camera) {
const auto actions = Actions(motion);
const auto index = NearestAt(actions, frame);
const auto *set = Lookup(motion.motion_list);
if (!set) {
return;
}
const auto index = NearestAt(set->actions, frame);
if (!index) {
return;
}
const auto &action = actions[*index];
const auto &action = set->actions[*index];
Apply(root, action, Local(action, frame), camera);
PosedActions().insert_or_assign(&root, Posed{.anchor = motion.motion_list, .action = *index});
PosedActions().insert_or_assign(&root, Posed{.anchor = motion.motion_list, .stamp = set->stamp, .action = *index});
}

void Blend(mgCFrame &root, tagMOTION_TYPE &motion, float from, float to, float blend, mgCCamera *camera) {
Expand All @@ -163,11 +237,15 @@ void Blend(mgCFrame &root, tagMOTION_TYPE &motion, float from, float to, float b
}
const auto &target = actions[*target_index];
const float target_local = Local(target, to);
// The game blends from the frame it last posed; that action may belong to another motion set.
// The game blends from the frame it last posed; that action may belong to another motion set. A set
// released or registered again since then no longer holds that action.
const Action *source = nullptr;
if (const auto posed = PosedActions().find(&root); posed != PosedActions().end()) {
if (const auto set = Registry().find(posed->second.anchor); set != Registry().end() && posed->second.action < set->second.size()) {
const auto &candidate = set->second[posed->second.action];
if (const auto found = PosedActions().find(&root); found != PosedActions().end()) {
// Lookup may forget a stale set and its poses, this one included, so the record is copied first.
const Posed posed = found->second;
const auto *set = Lookup(posed.anchor);
if (set && set->stamp == posed.stamp && posed.action < set->actions.size()) {
const auto &candidate = set->actions[posed.action];
if (from >= static_cast<float>(candidate.base) && from <= static_cast<float>(candidate.base) + candidate.duration + 1) {
source = &candidate;
}
Expand Down
21 changes: 20 additions & 1 deletion port/src/character_motion.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -30,14 +30,33 @@ struct Action {

/**
* Registers the actions of a motion set under its channel anchor.
*
* The storage is the arena memory holding the set's channels and anchor; the set stops resolving once any
* of it is handed out again. The anchor is also stamped, so a set whose anchor is overwritten without going
* through an arena stops resolving too.
*/
void Register(const Mot_List *anchor, std::vector<Action> actions);
void Register(Mot_List *anchor, std::vector<Action> actions, std::span<const std::byte> storage);

/**
* Forgets a motion set whose channel anchor is released or replaced.
*/
void Forget(const Mot_List *anchor);

/**
* Forgets every motion set whose storage overlaps memory about to be written again.
*
* The game releases motion memory by resetting or rewinding an arena, or by freeing a heap block, without
* telling the registry. The arena reports each range it hands out again or writes a header or an imported
* buffer over, and the main-thread file loads report their whole sector-padded destination, which they
* write before the caller reserves it.
*/
void ForgetStorage(const void *begin, const void *end);

/**
* Forgets the pose recorded on a frame tree whose root is initialized or whose address is reused.
*/
void ForgetFrame(const mgCFrame *root);

/**
* Forgets every registered motion set and recorded pose.
*/
Expand Down
15 changes: 15 additions & 0 deletions port/src/dataread.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@
#include <vector>

#include "bounded_text.hpp"
#include "character_motion.hpp"
#include "crash/crash.hpp"
#include "dataread.hpp"
#include "dataread_port.hpp"
Expand Down Expand Up @@ -97,6 +98,12 @@ void ZeroSectorTail(void *buffer, std::size_t size) {
std::memset(static_cast<char *>(buffer) + size, 0, PaddedSize(size) - size);
}

// A load writes its whole destination, sector padding included, before the caller reserves the unpadded size
// it reports. Main-thread loads tell the motion registry first, so it forgets the sets that lived there.
void ReportDestination(void *buffer, std::size_t bytes) {
character_motion::ForgetStorage(buffer, static_cast<char *>(buffer) + bytes);
}

bool IsExcludedRootName(std::string_view name) {
if (name.starts_with('.') || name.ends_with(".dc2tmp")) {
return true;
Expand Down Expand Up @@ -229,7 +236,11 @@ int ReadUnregistered(const IndexedFile &file, void *buffer) {
return *size;
}

// Reads a whole file into a sector-padded buffer and records the buffer; main thread only.
int ReadWhole(const IndexedFile &file, void *buffer) {
if (const std::optional<int> size = FileSize(file)) {
ReportDestination(buffer, PaddedSize(static_cast<std::size_t>(*size)));
}
const int size = ReadUnregistered(file, buffer);
if (size >= 0) {
loaded_buffers[buffer] = static_cast<std::size_t>(size);
Expand Down Expand Up @@ -257,6 +268,7 @@ int ReadSaveFile(const char *name, void *buffer, bool size_only) {
int size = sceLseek(fd, 0, SCE_SEEK_END);
bool ok = size >= 0;
if (ok && !size_only) {
ReportDestination(buffer, PaddedSize(static_cast<std::size_t>(size)));
ok = sceLseek(fd, 0, SCE_SEEK_SET) >= 0 && sceRead(fd, buffer, size) == size;
}
sceClose(fd);
Expand Down Expand Up @@ -291,6 +303,7 @@ int ResolveFile(const char *name, const std::string &key, void *buffer, bool siz
if (const auto module = luau_runtime::ModuleForScript(key)) {
const std::string handle = luau_runtime::MakeHandle(*module);
if (buffer && !size_only) {
ReportDestination(buffer, handle.size());
std::memcpy(buffer, handle.data(), handle.size());
}
return static_cast<int>(handle.size());
Expand Down Expand Up @@ -693,6 +706,7 @@ PORT_OVERRIDE int LoadFileBG(char *name, u_long128 *buffer, int *out_size) {
int size = 0;

if (CacheSlot *cached = CacheFind(CacheKey(name))) {
ReportDestination(buffer, static_cast<std::size_t>(cached->size));
PortTextureSourceCopy(buffer, cached->address, static_cast<std::size_t>(cached->size));
cached->ref_count--;
size = cached->size;
Expand Down Expand Up @@ -798,6 +812,7 @@ PORT_OVERRIDE int LoadFile2(char *path, void *buffer, int *out_size, int mode) {

if (CacheSlot *cached = CacheFind(CacheKey(path))) {
if (mode != LOAD_FILE_SIZE) {
ReportDestination(buffer, static_cast<std::size_t>(cached->size));
PortTextureSourceCopy(buffer, cached->address, static_cast<std::size_t>(cached->size));
}
if (mode == LOAD_FILE_READ) {
Expand Down
Loading
Loading