Recover recorded animation playback wrappers

This commit is contained in:
Jonas Volman committed 2026-08-27 20:18:09 -07:00
1 parent 859ebedaab
commit b97d95fe79
6 files changed
+375 -16

No files matched your search

@@ -0,0 +1,87 @@
# `recorded_animations.obj` playback worker and wrapper recovery (2026-08-27)
## Result
This wave advances `recorded_animations.obj` from 2/16 to 6/16 strict-exact
functions. It adds the private thread lookup and all three public playback
wrappers while preserving both accepted disposal functions.
| Strict function | Padded bytes | Relocations | Normalized SHA-256 |
| --- | ---: | ---: | --- |
| `_recorded_animations_dispose` | 48 | 4 | `b9b64bb030325c73231a51ab101f24b1d8ce3ec37789c76d2686751f0cc07ed0` |
| `_recorded_animations_dispose_from_old_map` | 16 | 2 | `9dedd614496101c84c04e338b0fb2167843b37380b3f0e2a50007ce9c2bc7edc` |
| `_code_000836c0` | 96 | 4 | `dfe89944f5a5133e3b05901bba5076f4bcda484539e5373342c7a97452e745b7` |
| `_recorded_animation_play` | 32 | 1 | `ef585acc1d2013cd265d49eaa84ecdaf199a4240338ab525e2e5415f55138fa2` |
| `_recorded_animation_play_and_delete` | 32 | 1 | `35ec6175e1878120e0ea7fdb6a81827fd940b345897631bc2401ac44c32740ea` |
| `_recorded_animation_play_and_hover` | 32 | 1 | `c11e67670227db5072be50fc141406af4628cdaabce20cdbd8b1377aed52d2a3` |
The four new owners contribute 151 meaningful bytes: 85 bytes for the lookup
and 22 bytes for each wrapper. Including the two disposal owners, the object
now has 200/2,684 meaningful code bytes exact.
The recovered `_code_000839a0` playback worker is a real typed implementation,
not a wrapper-enabling stub. It is 640/640 padded bytes, has the target's 38
relocations and the same relocation destinations in the same semantic order,
and emits the exact target assertion expression and error strings. Its
normalized instruction bytes remain residual because the compiler assigns the
long-lived unit and animation-index values to the opposite ESI/EDI roles and
schedules several typed field loads differently. That residual is retained as
the lawful foundation for the remaining object.
## Evidence and history audit
Before editing production, the complete reachable source history for
`source/cutscene/recorded_animations.c` was swept through the scratch compiler.
There were three unique blobs; the cumulative source was already best at 2/16.
The historical `jonas/recorded-animation-wrappers-wave-20260820` tree contained
only the symbol skeleton, not hidden implementations.
The prior accepted ledgers were read in full:
- `recorded_animations_obj_jonas_dispose_old_map_20260821.md`;
- `recorded_animations_obj_jonas_dispose_first_shot_20260826.md`.
Their private-EAX warning was decisive: compiling the public donor wrappers as
ordinary external forwards is not target-authentic. The worker and lookup had
to exist in the same translation unit so VC7 could derive the private register
ABI naturally.
Semantic source came from the pinned local donor mirrors:
- `build/audit/refs/halocea/src/blam/cutscene/recorded_animation_play_internal.c`;
- `build/audit/refs/halocea/src/blam/memory/get_controlling_thread.c`;
- the three HCEA playback wrapper sources under
`build/audit/refs/halocea/src/blam/cutscene/`;
- `build/audit/refs/stian-halo/src/halo/cutscene/recorded_animations.c`.
January split COFF and relocation-annotated disassembly remained the authority
for control-flow order, field widths, private ABI, exact strings, calls,
relocations, and padding. In particular, the January helper's loop is a
`while (thread)` scan with an equality break. Expressing that ordinary source
topology made `_code_000836c0` strict exact and simultaneously gave all three
wrappers their exact EAX-plus-stack call shape.
Claude's active worktrees were read only. No file, index, object, or build
artifact in those worktrees was modified.
## Policy and validation boundary
The retained implementation uses typed structures for the animation thread,
debug entry, playback codec table, scenario block, and data-array iterator. It
contains no assembly, volatile/register steering, pragma, intrinsic,
attribute, barrier, raw address, raw byte-offset access, pointer/integer
reconstruction, punning, undefined behavior, synthetic anchor, payload, or
object-byte forcing. All declarations are C89-compatible and every function
has an explicit return.
The scratch gate reports 6 exact, one residual, and nine unwritten functions.
Direct production-object comparison proves all six accepted owners have equal
padded bytes plus ordered relocation address/type/destination/addend identity.
The final frozen source was rebuilt through one normal selected-object Ninja
edge for the helper/CFG wave.
The campaign gate and branch sweep were also made concurrency-safe during this
audit: alternate branch blobs now compile through `gate.py --source` from a
PID-scoped scratch source. `branch_sweep.py` no longer overwrites or restores
the production source, so it cannot collide with Claude or another active
worker.
@@ -0,0 +1,25 @@
# `wind.obj` axis-countdown refinement (2026-08-27)
## Result
`wind.obj` remains 7/9 strict-exact functions, with `_code_0017f750` residual
and `_code_0017fbd0` unwritten. The retained ordinary-C change replaces the
three-axis index comparison with the target-backed two-role loop topology:
`axis_index` selects the variance row while `axes_remaining` is the countdown.
The candidate remains 208 meaningful/padded bytes versus the target's 224 and
retains all eight target relocation destinations. Its relocation-normalized
differing-byte distance improves from 193 to 124. The target loop prologue,
`mov ebx,3`, countdown `dec ebx`, and back edge now agree.
The remaining principal difference is the January compiler's full-width
sign-bit clearing sequence around the floating sample key. The lawful source
still canonicalizes `fabs` to the x87 `fabs` instruction. A split assignment
was scratch-tested and produced the same x87 form. The historical exact
experiment used inactive-union access and remains rejected; byte
representation writes are outside campaign policy.
All current wind ledgers and reachable source-history blobs were audited before
retention. The final gate reports seven exact, one residual, and one unwritten
function, and the frozen source was rebuilt through one normal selected-object
Ninja edge. No accepted sibling changed.
+240 -2
View File
@@ -87,14 +87,72 @@ symbols in this file:
/* ---------- headers */
#include "cseries/cseries.h"
#include "cseries/errors.h"
#include "cutscene/recorded_animation_definitions.h"
#include "game/players.h"
#include "memory/data.h"
#include "objects/objects.h"
#include "scenario/scenario.h"
#include "scenario/scenario_definitions.h"
#include "units/units.h"
/* ---------- constants */
/* ---------- macros */
enum
{
_recording_thread_finished_bit = 0,
_recording_thread_unit_was_controlled_bit = 2,
RECORDED_ANIMATION_VERSION = 4,
};
/* ---------- structures */
struct animation_thread
{
short identifier;
short pad2;
long unit_index;
word ticks_left;
word flags;
long relative_ticks;
byte *event_stream;
byte controller[0x40];
byte animation_state[0xC];
short version;
short pad62;
};
struct animation_thread_debug
{
boolean active;
byte pad1[3];
byte *event_stream;
long event_stream_size;
short animation_index;
short padE;
};
struct animation_playback
{
void (*initialize_event_stream)(
void *animation_state,
void *controller,
byte **event_stream,
byte unit_control_data_version);
};
struct recorded_animation_playback_data
{
void *unused[4];
struct animation_playback *playback_codec[RECORDED_ANIMATION_VERSION];
};
/* ---------- macros */
#define animation_threads_debug_get(index) \
(animation_threads_debug + ((index) & 0xFFFF))
#define playback_codec (data_002dd160.playback_codec)
struct recorded_animations_globals_prefix
{
struct data_array *animation_threads;
@@ -102,10 +160,22 @@ struct recorded_animations_globals_prefix
/* ---------- prototypes */
static struct animation_thread *code_000836c0(
long unit_index,
long *thread_index_reference);
static boolean code_000839a0(
long unit_index,
short animation_index,
word extra_flags);
extern boolean recorded_animation_controlling_unit(
long unit_index);
/* ---------- globals */
extern void *animation_threads_debug;
extern struct animation_thread_debug *animation_threads_debug;
extern struct recorded_animations_globals_prefix bss_00435ca4;
extern struct recorded_animation_playback_data data_002dd160;
/* ---------- public code */
@@ -132,4 +202,172 @@ void recorded_animations_dispose_from_old_map(
return;
}
boolean recorded_animation_play(
long unit_index,
short animation_index)
{
return code_000839a0(unit_index, animation_index, 0);
}
boolean recorded_animation_play_and_delete(
long unit_index,
short animation_index)
{
return code_000839a0(unit_index, animation_index, 1 << 3);
}
boolean recorded_animation_play_and_hover(
long unit_index,
short animation_index)
{
return code_000839a0(unit_index, animation_index, 1 << 4);
}
/* ---------- private code */
static struct animation_thread *code_000836c0(
long unit_index,
long *thread_index_reference)
{
long thread_index = NONE;
struct data_iterator iterator;
struct animation_thread *thread;
data_iterator_new(&iterator, bss_00435ca4.animation_threads);
thread = data_iterator_next(&iterator);
while (thread)
{
if (thread->unit_index == unit_index)
{
thread_index = iterator.datum_index;
break;
}
thread = data_iterator_next(&iterator);
}
if (thread_index_reference)
*thread_index_reference = thread_index;
return thread;
}
static boolean code_000839a0(
long unit_index,
short animation_index,
word extra_flags)
{
boolean result = FALSE;
long thread_index;
struct animation_thread *thread;
struct recorded_animation_definition const *animation;
struct animation_thread_debug *debug;
struct animation_playback *playback;
if (unit_index != NONE)
{
if (animation_index != NONE && animation_index < global_scenario_get()->recorded_animations.count)
{
object_get_and_verify_type(unit_index, _object_mask_unit);
player_index_from_unit_index(unit_index);
thread = code_000836c0(unit_index, &thread_index);
animation = TAG_BLOCK_GET_ELEMENT(
&global_scenario_get()->recorded_animations,
animation_index,
struct recorded_animation_definition);
if (!recorded_animation_controlling_unit(unit_index))
{
if (!thread)
{
thread_index = datum_new(bss_00435ca4.animation_threads);
if (thread_index != NONE)
thread = datum_get(bss_00435ca4.animation_threads, thread_index);
}
if (thread)
{
thread->unit_index = unit_index;
thread->relative_ticks = 0;
thread->ticks_left = animation->length_in_ticks;
thread->event_stream = tag_data_get_pointer(
&animation->event_stream,
0,
animation->event_stream.size);
debug = animation_threads_debug_get(thread_index);
debug->active = TRUE;
debug->event_stream = thread->event_stream;
debug->event_stream_size = animation->event_stream.size;
debug->animation_index = animation_index;
thread->version = animation->version - 1;
thread->flags &= ~(1 << _recording_thread_finished_bit);
match_assert(
"c:\\halo\\SOURCE\\cutscene\\recorded_animations.c",
233,
animation->version>0&&animation->version<=RECORDED_ANIMATION_VERSION&&playback_codec[animation->version-1]);
playback = playback_codec[thread->version];
playback->initialize_event_stream(
thread->animation_state,
thread->controller,
&thread->event_stream,
animation->unit_control_data_version);
unit_set_actively_controlled(unit_index, TRUE);
if (unit_controllable(unit_index))
thread->flags |= 1 << _recording_thread_unit_was_controlled_bit;
else
thread->flags &= ~(1 << _recording_thread_unit_was_controlled_bit);
unit_set_controllable(unit_index, FALSE);
unit_set_possessed(unit_index, TRUE);
object_set_automatic_deactivation(unit_index, FALSE);
thread->flags |= extra_flags;
result = TRUE;
}
else
{
error(_error_silent, "Could not allocate space for a new animation");
}
}
else if (thread)
{
char const *playing_name;
debug = animation_threads_debug_get(thread_index);
playing_name = "<unknown>";
if (debug->active)
{
playing_name = TAG_BLOCK_GET_ELEMENT(
&global_scenario_get()->recorded_animations,
debug->animation_index,
struct recorded_animation_definition)->name;
}
error(
_error_silent,
"trying to play %s while %s is playing",
animation->name,
playing_name);
}
else
{
error(_error_silent, "can't play animation on unit");
}
}
else
{
error(_error_silent, "this animation doesn't exist");
}
}
else
{
error(_error_silent, "unit doesn't exist");
}
return result;
}
+5 -1
View File
@@ -137,9 +137,11 @@ static void code_0017f750(
real const axis_scale[3] = { 0.1f, 0.2f, 0.07f };
real magnitude = maximum_magnitude * (1.f / 3.f);
long axis_index;
long axes_remaining = 3;
*wind = *global_zero_vector3d;
for (axis_index = 0; axis_index < 3; axis_index++)
axis_index = 0;
do
{
real sample_key = (real)fabs(
((axis_scale[axis_index] * wind_globals.time * local_variation_rate)
@@ -151,7 +153,9 @@ static void code_0017f750(
wind->i += wind_globals.variance[axis_index][(short)sample_index].i;
wind->j += wind_globals.variance[axis_index][(short)sample_index].j;
wind->k += wind_globals.variance[axis_index][(short)sample_index].k;
axis_index++;
}
while (--axes_remaining);
wind->i *= magnitude;
wind->j *= magnitude;
+14 -11
View File
@@ -1,5 +1,5 @@
"""For each named unit, try every branch's version of its .c file and report
any version that gates strictly better than the one on this branch.
"""For each named unit, compile every branch's version of its .c file from
scratch and report any version that gates strictly better than the current one.
Four objects were advanced this session by finding work already done in another
lane; this automates the search.
@@ -23,8 +23,11 @@ def unique_blobs(path):
return blobs
def gate(unit):
r = subprocess.run(GATE + [unit], capture_output=True, text=True)
def gate(unit, source=None):
command = GATE + [unit]
if source:
command += ['--source', source]
r = subprocess.run(command, capture_output=True, text=True)
if 'COMPILE FAILED' in r.stdout:
return None, 'compile-fail'
m = re.search(r'== exact (\d+)\s+residual (\d+)\s+unwritten (\d+)', r.stdout)
@@ -49,27 +52,27 @@ def sweep(unit):
return
cur = subprocess.run(['git', 'rev-parse', f'HEAD:{path}'], capture_output=True, text=True).stdout.strip()
blobs = unique_blobs(path)
orig = open(path, 'rb').read()
base_score, base_desc = gate(unit)
print(f'\n=== {unit} ({len(blobs)} unique blobs) current: {base_desc}', flush=True)
best = (base_score, 'CURRENT', base_desc)
scratch_source = f'scratch/_branch_sweep_{os.getpid()}.c'
try:
for blob, brs in blobs.items():
if blob == cur:
continue
content = subprocess.run(['git', 'cat-file', 'blob', blob], capture_output=True).stdout
open(path, 'wb').write(content)
score, desc = gate(unit)
open(scratch_source, 'wb').write(content)
score, desc = gate(unit, scratch_source)
tag = brs[0] + (f' (+{len(brs)-1})' if len(brs) > 1 else '')
better = score is not None and base_score is not None and score > base_score
print(f' {blob[:8]} {tag:52s} {desc}{" <== BETTER" if better else ""}', flush=True)
if score is not None and (best[0] is None or score > best[0]):
best = (score, tag, desc)
finally:
open(path, 'wb').write(orig)
# no ninja here: gate.py compiles to its own scratch object, so the sweep
# never reads build/base. Skipping it keeps concurrent sweeps safe (one
# ninja per build dir at a time) - run a full ninja when the sweep ends.
if os.path.exists(scratch_source):
os.remove(scratch_source)
# The sweep never edits the production source or reads build/base.
# gate.py compiles each branch blob to its own scratch object.
if best[1] != 'CURRENT':
print(f' >>> BEST: {best[1]} {best[2]}', flush=True)
+4 -2
View File
@@ -9,6 +9,7 @@ import coff_compare as cc
ap = argparse.ArgumentParser()
ap.add_argument('unit')
ap.add_argument('--source')
ap.add_argument('--edits')
ap.add_argument('--fn', action='append', default=[])
ap.add_argument('--disas')
@@ -16,7 +17,8 @@ ap.add_argument('--all', action='store_true')
a = ap.parse_args()
unit = a.unit.replace('\\', '/')
src = unit + '.c'
unit_src = unit + '.c'
src = a.source or unit_src
bn = open('build.ninja').read()
# the "build <obj>: cl <src>" line may be wrapped with a $ continuation, so match
# on the object path alone rather than requiring the leading "build " on that line
@@ -42,7 +44,7 @@ obj = f'scratch/_gate_{os.getpid()}.obj'
if os.path.exists(obj):
os.remove(obj)
cl = r'C:\halo-worktrees\claude-finish-hs-20260816\xbox\bin\vc7\CL.Exe'
incdir = os.path.dirname(src)
incdir = os.path.dirname(unit_src)
cmd = [cl, '/nologo', '/c'] + toks + ['/I' + incdir, '/Fo' + obj, scratch_src]
r = subprocess.run(cmd, capture_output=True, text=True)
if not os.path.exists(obj) or r.returncode != 0: