Skip to content

Make Object pointer-sized#23542

Open
SuchAFuriousDeath wants to merge 12 commits intoruffle-rs:masterfrom
SuchAFuriousDeath:object-refactor
Open

Make Object pointer-sized#23542
SuchAFuriousDeath wants to merge 12 commits intoruffle-rs:masterfrom
SuchAFuriousDeath:object-refactor

Conversation

@SuchAFuriousDeath
Copy link
Copy Markdown
Collaborator

@SuchAFuriousDeath SuchAFuriousDeath commented Apr 26, 2026

This is a working preview of the long-awaited (and promised) Object refactor.

The design is 90% me + 10% advice from Codex to fill in the gaps that I couldn't quite get right. Most of the actual code changes were done by Claude Code, because they're mostly mechanical, I iterated with him until I was reasonably happy with everything.
I'm not fully satisfied with the design and I don't expect this to get merged without any adjustments but I have to post it here to get more eyes on it, or it won't get finished at all.

Imperfections:

  1. define_variants and the definition of the Object through the tagged_trait_object, that is one thing I can't quite figure out. Imo the tagged_trait_object should macro should not exist at all.
  2. the way it is now, it's not reusable for stuff like DisplayObject (and similar enums). I also can't quite figure out how to do this in a satisfying way (and without making it complicated and confusing).
  3. Probably some other stuff too but I don't remember anymore :D

As a sidenote, with the traits that currently exist in this PR, I think that handles (Stuff like ObjectHandle, NetStreamHandle, ScriptObjectHandle, MovieClipHandle and FileReferenceObjectHandle) could be generalized rather than having one type for each stashable Object type. So that's potentially something nice for the future.

Also, review commit-by-commit to preserve sanity.

@SuchAFuriousDeath SuchAFuriousDeath added code-cleanup General improvement of the code base waiting-on-review Waiting on review from a Ruffle team member T-refactor Type: Refactor / Cleanup labels Apr 26, 2026
@kjarosh kjarosh added the A-avm2 Area: AVM2 (ActionScript 3) label Apr 26, 2026
@Lord-McSweeney
Copy link
Copy Markdown
Collaborator

This worsens a getslot microbenchmark of mine by ~10%. Here's the codegen for op_get_slot with this PR vs on current master:

Master:

  sub    $0x18,%rsp
  mov    %rdi,%rax
  mov    0x18(%rdi),%rcx
  mov    0x20(%rdi),%rdi
  dec    %rdi
  cmp    %rcx,%rdi
  jae    a8
  mov    0x10(%rax),%rcx
  shl    $0x4,%rdi
  movups (%rcx,%rdi,1),%xmm0
  movaps %xmm0,(%rsp)
  mov    (%rsp),%rdx
  lea    -0x2d(%rdx),%r8
  cmp    $0x1,%r8
  jbe    63
  cmp    $0x2c,%rdx
  ja     76
  mov    0x8(%rsp),%rax
  cmp    0x18(%rax),%rsi
  jae    8f
  add    %rdi,%rcx
  mov    0x10(%rax),%rax
  shl    $0x4,%rsi
  movups (%rax,%rsi,1),%xmm0
  movups %xmm0,(%rcx)
  xor    %eax,%eax
  add    $0x18,%rsp
  retq
(error/panic cases)

With this PR:

  mov    %rdi,%rax
  mov    0x18(%rdi),%rcx
  mov    0x20(%rdi),%rdi
  dec    %rdi
  cmp    %rcx,%rdi
  jae    a7
  mov    0x10(%rax),%rcx
  shl    $0x4,%rdi
  movups (%rcx,%rdi,1),%xmm0
  movaps %xmm0,(%rsp)
  movzbl (%rsp),%edx
  cmp    $0x1,%dl
  jbe    62
  cmp    $0x6,%dl
  jne    75
  mov    0x8(%rsp),%rax
  test   %rax,%rax
  je     75
  cmp    0x58(%rax),%rsi
  jae    8e
  add    %rdi,%rcx
  mov    0x50(%rax),%rax
  shl    $0x4,%rsi
  movups (%rax,%rsi,1),%xmm0
  movups %xmm0,(%rcx)
  xor    %eax,%eax
  add    $0x18,%rsp
  retq
(error/panic cases)                                                                      

Moves `new` and `custom_new` from the Erased-defaulted `impl<'gc>` block
onto the generic `impl<'gc, K>` block so that each caller's inferred `K`
(taken from the field or let binding context) determines the resulting
`ScriptObjectData<'gc, K>` type. No callers need to change today: every
existing call site assigns into a `ScriptObjectData<'gc>` field which
picks up the `K = Erased` default exactly as before.

Also switches the repr from `align(8)` to `#[repr(C, align(8))]` so that
every `ScriptObjectData<'gc, K>` has a guaranteed identical in-memory
layout regardless of `K`, and adds an `erase_kind` inherent helper that
casts `Gc<Self>` to `Gc<ScriptObjectData<'gc, Erased>>` on the back of
that guarantee. Per-variant migrations will use the helper in their
`gc_base` implementations once their `base` fields become typed.
For every variant, `base: ScriptObjectData<'gc>` becomes
`base: ScriptObjectData<'gc, kind::X>`, and `gc_base` wraps the
`HasPrefixField::as_prefix_gc` call in `erase_kind`. `ScriptObject`'s
direct `Gc<ScriptObjectData<'gc>>` inner becomes
`Gc<ScriptObjectData<'gc, kind::ScriptObject>>`, with the one internal
wrap-into-ScriptObjectWrapper site erasing explicitly.

Constructor call sites are unchanged; the type parameter is inferred
from the destination field.
Adds a runtime `kind: ObjectKind` field, tightens `new` and `custom_new`
to require `K: Kind`, and sets `kind: K::ID` in the struct literal.

Since every variant's `base` field is typed with its matching kind
marker, the stored tag is structurally guaranteed to match the variant;
no caller passes it and no caller can get it wrong.
Every type is trivially a prefix of itself. The cast is a no-op — no
alignment check or offset assertion needed.

Needed by the upcoming `ObjectVariant` impl for `ScriptObject`, whose
`Data` type is `ScriptObjectData<'gc, kind::ScriptObject>` itself (with
no wrapper `*Data` struct), so the `HasPrefixField<Data>` bound on
`ObjectVariant::Data` can only be satisfied by an identity impl.
Adds a `tagged_trait_object` proc macro (sibling of `enum_trait_object`)
that emits a `#[repr(transparent)]` struct wrapping
`Gc<ScriptObjectData<Erased>>` instead of an enum. Method dispatch
matches on `self.0.kind()` and casts through
`crate::avm2::object::cast::downcast_unchecked`; `From<Variant>` impls
go through `cast::upcast`.

`Object` itself is switched to this representation, making it
pointer-sized (8 bytes) rather than 16 bytes.

`impl_downcast_methods!` and `define_weak_enum!` are updated to cast
through the new helpers instead of matching enum variants.
Replaces the `WeakObject` enum with a `#[repr(transparent)]` struct
wrapping `GcWeak<ScriptObjectData<Erased>>`. Upgrade and downgrade
become trivial pointer-manipulation operations — no variant dispatch is
needed, because the kind tag stays with the live allocation and an
upgraded `WeakObject` yields an `Object` directly.

Removes the `define_weak_enum!` macro and the 45 typed
`\w+ObjectWeak` wrappers it relied on; none were used outside the
enum definition.
…in the first cache line

`#[repr(C)]` lays fields out in declaration order, so the previous
ordering put the large `RefLock<DynamicMap<…>>` `values` field first and
pushed `slots: Box<[Lock<Value<'gc>>]>` out to offset 64 — the second
cache line of `GcBoxInner`'s value region. `op_get_slot` / `op_set_slot`
then needed an extra cache line per access, costing measurable
performance.

Move `slots` and `vtable` to the front (right after `kind`) so that
they share the first cache line with the GC header reads. Layout is
otherwise unaffected — `kind` stays at offset 0 (required by the
`tagged_trait_object` cast machinery), and `_kind: PhantomData` stays
at the end.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

A-avm2 Area: AVM2 (ActionScript 3) code-cleanup General improvement of the code base T-refactor Type: Refactor / Cleanup waiting-on-review Waiting on review from a Ruffle team member

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants