Getting Started Welcome Setting Up Game Tree GuiRoot Method
Writing a Memory Library Reading and Writing Page Iterator Pattern Scanning Buffer Strategies
Writing a Roblox Library C++ String

Buffer Strategies

In this guide, we will optimize reads and writes by batching them to reduce syscall overhead

The Problem

Each call to NtReadVirtualMemory or NtWriteVirtualMemory requires a syscall — a context switch from user mode to kernel mode.
This is expensive. Reading 100 individual u32s one at a time is roughly 100x slower than reading a single 400-byte block and parsing it in userspace.
The overhead is not in the data transfer; it is in the transition itself. Minimizing syscalls is the single largest optimization you can make.

flowchart LR subgraph "Naive (slow)" A1[Read 8 bytes] --> A2[Read 8 bytes] --> A3[Read 8 bytes] end subgraph "Batched (fast)" B1[Read 24 bytes] --> B2[Parse in userspace] end

Read Everything at Once

Instead of reading every field, simply read the entire data structure you need as a single batch. This avoids having to do multiple syscalls and context switches and is often simpler to implement.

// Instead of reading one field at a time:
const name_ptr = try memory_accessor.read(instance_ptr + Config.get("Offset/Instance/Name"), usize);
const str_ptr = try memory_accessor.read(name_ptr, usize);
const str_len = try memory_accessor.read(name_ptr + @sizeOf(usize) * 2, usize);
const str_cap = try memory_accessor.read(name_ptr + @sizeOf(usize) * 3, usize);
if(str_cap < 16) {
    const str_data = try memory_accessor.read(name_ptr, [16]u8);
    std.debug.print("Got short string: {s}\n", .{ str_data[0..str_len] });
} else {
    const str_data = try allocator.alloc(u8, str_len);
    defer allocator.free(str_data);
    try memory_accessor.readSlice(str_ptr, u8, str_data);
    std.debug.print("Got long string: {s}\n", .{ str_data });
}

// Read the entire data structure at once:
const StringData = extern struct {
    sso: extern union {
        short: [16]u8,
        ptr: usize,
    },
    len: usize,
    capacity: usize,
};
const string_data = try memory_accessor.read(name_ptr, StringData);

// Now read each field locally - zero syscalls when reading the struct.
if(string_data.capacity < 16) {
    std.debug.print("Got short string: {s}\n", .{ string_data.sso.short[0..string_data.len] });
} else {
    const str_data = try allocator.alloc(u8, str_len);
    defer allocator.free(str_data);
    try memory_accessor.readSlice(string_data.sso.ptr, u8, str_data);
    std.debug.print("Got long string: {s}\n", .{ str_data });
}

For collections like vectors of children, the same principle applies - one syscall for the vector header, one syscall for all elements, then pure local iteration:

pub fn doSomethingWithChildren(mem: MemoryAccessor, instance_ptr: usize, allocator: std.mem.Allocator) !void {
    // Read vector (24 bytes) in one call
    const VectorLayout = extern struct {
        begin: usize,
        end: usize,
        // capacity: usize - Read only what you need, you don't need capacity here so don't read it!
    };
    // First read vector layout (1 syscall)
    const vec: VectorLayout = try mem.read(instance_ptr + Config.get("Offset/Instance/Children"), VectorLayout);
    
    const InstanceSharedPtr = extern struct {
        ptr: usize,
        count: usize,
    };
    const children = try allocator.alloc(InstanceSharedPtr, (vec.end - vec.begin)/@sizeOf(InstanceSharedPtr));
    defer allocator.free(children);

    // Read ALL children in one syscall
    try mem.readSlice(vec.begin, InstanceSharedPtr, children);

    // Now iterate locally
    for (children) |child| {
        // ...
    }
}
flowchart LR A["One syscall: read vector header"] --> B["One syscall: read all N elements"] B --> C["Iterate locally: zero syscalls"]

Batch Writes

Writes can also be batched. Instead of issuing one syscall per write, queue the writes in a local buffer and flush them all at once.
This is especially useful when patching many small values in a tight loop.

const fill_me_with_null = 0xDEADBEEFABADC0DE;

// Instead of:
for(0..4096) |i|
    try memory_accessor.write(fill_me_with_null + i, @as(u8, 0));

// Try:
const null_buf: [4096]u8 = @splat(0);
try memory_accessor.write(fill_me_with_null, null_buf);

Use Page Iterator for Bulk Scanning

As covered in the Page Iterator chapter, the most efficient scanning strategy is to iterate over each committed page, read it entirely into a local buffer with a single syscall, then scan that buffer in userspace.
This means zero syscalls during the scan itself — every comparison, every pointer arithmetic operation happens entirely in your own process.