Lab3 Studio← lab3.studio

An Unreal Engine plugin by Lab³ Studio

BP Nativizer

Pick your hottest Blueprint functions, turn them into C++ you can actually read, and prove the two behave identically before anything ships.

BP Nativizer compiles a function's Blueprint bytecode into C++ and binds the native body onto the live function, so every existing Blueprint caller just starts running it. Your Blueprint stays the source of truth: anything the tool can't prove, it refuses out loud, and the moment you edit a nativized function it falls back to the VM until you regenerate.

v1.0.0Unreal Engine 5.8Bytecode-exactNo LLM, no approximation

Why use it

Blueprint is a great place to write gameplay logic and a terrible place to run the hottest two percent of it. The usual fix is rewriting those functions in C++ by hand. Here's how that actually compares:

Source of truthThe rewrite becomes the real implementation; the Blueprint gets deleted, or worse, quietly driftsThe Blueprint stays authoritative. Generated code is derived output, rebuilt from the bytecode whenever you ask
CorrectnessSomeone eyeballs the diff, and the differences turn up in playtestsThe parity harness throws the same random inputs at both versions and compares every output, member by member, objects by content
CallersYou hunt down every Blueprint caller and repoint itCallers do not change at all - the native body is bound onto the live UFunction they already call
Blueprint editsThe rewrite quietly stops matching what the designer meantThe source hash notices, and the function runs on the VM again until you regenerate
CoverageAnything, if you have the hoursWhatever the emitter can prove. The rest refuses by name and keeps running on the VM, untouched
EffortHours per functionA checkbox in the Details panel, then one project build
UndoDig the old Blueprint out of version control and repoint everything againUntick the checkbox, or delete the generated plugin outright - every function returns to the VM and nothing else breaks

For a sense of scale: on the project this was built for, nativizing the AI fast cycle saved a mean 0.93 ms (−29%) on the game-thread timer slice with 30 NPCs up. Measure your own functions with the built-in Measure command, in PIE, before and after - performance numbers transfer between projects less than anyone hopes.

Features

Bytecode-exact loweringThe emitter works from the Blueprint's compiled bytecode, not its node graph, so what runs natively is exactly what the VM would have executed. Anything it cannot prove becomes a named refusal at generation. It never guesses.
Randomized parity verificationVerify runs up to hundreds of thousands of randomized cases through the Blueprint VM and the native body, comparing every output member and objects by content, with branch coverage reported per input. A function earns trust by being measured, not by compiling.
A hash that means the same thing everywhereThe canonical source hash belongs to the Blueprint, not to the process that read it - identical in the editor, a commandlet, and a cooked build. Instrumentation, FName casing, and build-configuration sizes are all excluded, each one because it was caught making the same function hash two different ways.
Everything fails toward the VMProperty offsets, struct layouts, and external signatures are resolved at bind time against the live types. If the hash, the layout, or a signature stops matching, the Blueprint VM keeps the function. Unbind puts the bytecode back exactly as it was.
A generated plugin you can read and deleteGenerated functions live in their own plugin as readable C++, one file per function, with the source Blueprint and hash in each header. Writes are transactional with journaled crash recovery - and deleting the whole plugin is a supported panic button, not a catastrophe.
Editor and headless surfaceA Nativize checkbox with live status in the Details panel, a toolbar dropdown, console commands, and a commandlet that drives everything unattended - sweep, generate, verify, measure, and the generated plugin's whole lifecycle.
Project-wide eligibility sweepOne read-only commandlet run decodes every Blueprint function under a content path and tells you what would nativize today, what refuses, and why - as Markdown and CSV. Worth running before you commit to anything.

Note

Not everything nativizes, on purpose.

The emitter covers deliberately less than the whole Blueprint VM, and everything it refuses keeps running on the VM exactly as before. Before you buy into a workflow, run the sweep commandlet on your project - it tells you the exact split for your own content.

Getting Started

  1. Copy the BPNativizer folder into your project's Plugins folder.
  2. In the Unreal Editor, go to Edit > Plugins and enable BP Nativizer. There are no plugin dependencies.
  3. Restart the editor. For C++ projects, regenerate project files and rebuild.
  4. Open a Blueprint, select a function, and tick Nativize in the Details panel. The plugin generates the C++ into Plugins/NativizedFunctions (created on first use) and says build required.
  5. Close the editor, build, reopen. The log reports bound N generated function(s).
  6. Press Verify on the function before you trust it.

Good candidates

Anything called every frame for every agent: AI service ticks, interpolation updates, path evaluation, state scoring. That's where per-call VM overhead piles up. A function that runs once per interaction isn't worth the checkbox.

The generated code

One C++ file per function, sitting in your project, meant to be committed and reviewed like anything else you wrote. The header tells you which Blueprint it came from and under which hash; the body follows the bytecode statement for statement and keeps the Blueprint's own variable names, so it reads like the graph you built.

// GENERATED BY BPNativizer - DO NOT EDIT.
// Source function: /Game/Blueprints/AI/AC_AIRuntime.AC_AIRuntime_C::EvaluatePath
// Source hash:     0x4BDF211135D0B82C
// Bytecode:        1301 bytes, 51 statements

static void Body(UObject* Context, void* Parms)
{
    ...
    L_isValidPath = true;
    if (!(A_Reset)) { goto Lbl_12; }
    L_CallFunc_Array_Length_ReturnValue_1 = (V_PathPoints).Num();
    L_CallFunc_Greater_IntInt_ReturnValue =
        UKismetMathLibrary::Greater_IntInt(L_CallFunc_Array_Length_ReturnValue_1, 0);
    if (!(L_CallFunc_Greater_IntInt_ReturnValue)) { goto Lbl_33; }
    ...
}

Member offsets and external signatures are resolved at bind time against the live types, never baked in - so a struct edit or a signature change refuses to bind instead of silently reading the wrong bytes. Each file also carries a call counter, which means "did my Blueprint caller actually reach native code" has a numeric answer instead of a hopeful one.

Headless Reference

Everything the UI does, one commandlet does unattended - the Details-panel checkbox with Verify / Measure / Regenerate, the toolbar dropdown, and the bpnativizer.* console family all drive the same code underneath, so nothing you script behaves differently from what you click.

UnrealEditor-Cmd.exe YourProject.uproject -run=BPNativizer -mode=verify -class=/Game/Path/BP_MyActor -function=MyFunction -iterations=100000 -unattended -nopause -nosplash
sweepDecodes every Blueprint function under a content path and asks both gates about each. Writes Saved/BPNativizer/sweep.md and sweep.csv. Read-only.
validateThe node gate's complete verdict on one function, naming every unsupported node.
tick / untickNativizes or reverts one function, exactly as the checkbox does.
verifyRandomized Blueprint-versus-native parity. Default 100,000 cases; seedable.
bindThe 11-invariant bind round trip: install, replay identical cases, unbind, check restoration.
measurePer-call timing, VM versus native. Measure in PIE for numbers that transfer; commandlet figures run about half the editor's.
dumpDisassembly, canonical form, and hash for one function, written to Saved/BPNativizer/.
identity / dupregSelf-contained hostile suites: the type-retype matrix and registration ownership under hot-reload ordering. Run anywhere.
bootstrap / validate-generated / repair / remove / cleanupThe generated plugin's lifecycle, scriptable. validate-generated exits 0 clean, 2 drift.

What v1 refuses

You should be able to tell whether this fits your project before you buy it. These construct families refuse today; a function that uses one keeps running on the Blueprint VM and behaves exactly as it always did.

CustomThunk wildcard functionsLibrary functions with no derivable C++ signature - some Map/Set, Chooser, and DataTable utilities.
Text valuesTextProperty carries localization semantics v1 does not model.
Native struct constants with unpaired membersAlmost always anim-Blueprint internals - functions nobody should nativize anyway.
Non-object reads through a contextReading a float or string off another object. Object references work, and so does one struct property deep - the brush-off-another-widget shape.
Latent and async nodesDelay, timelines, async task nodes. Latency has no synchronous C++ equivalent.
Delegate bind / add / remove / clearBroadcast IS supported; mutation of delegate bindings is not.
Editor-only dependenciesA function calling into an editor-only module refuses rather than breaking your packaged build. A protection, not a gap.

Loop macros - ForEachLoop, WhileLoop and friends - are supported, nested included: they lower to the same execution-flow stack the VM runs. For a sense of what the refusals leave: on the 353-Blueprint Lyra project the release gates run against, 211 of 308 function bodies - just over two thirds - nativize today.

Verification

This table is what the release gates actually verify - the right column is unverified, which is not the same as broken. None of it asks you to take our word for anything: the self-test suites (identity, dupreg) and the parity harness ship inside the plugin and run headless against your own functions, on your own machine.

EngineUnreal Engine 5.8, exactly. The decoder mirrors 5.8's script serialization case for case.5.7 and earlier, 5.9 and later. Do not use this version on another engine release.
TargetsEditor and Game, Development and Shipping, through an isolated BuildPlugin run with -StrictIncludesClient, Server, and Commandlet targets, which require a source engine build
PlatformsWin64Linux, Mac, consoles, and mobile. The plugin contains no platform-specific code, but these platforms have not been verified.
Bytecode coverageA full-content sweep of a Lyra-based project: 353 Blueprints, 308 function bodies decoded, zero load or decode failures, 211 (68.5%) eligible todayContent using constructs the sweep has not seen; run the sweep on your own project
ParityTwenty-one live project functions - every manifest entry - spanning loop macros (a nested ForEach accumulator by name), arrays as members, locals and parameters (out-of-bounds included), Blueprint structs, delegate broadcast, by-name dispatch through call-result and library contexts, property reads off other objects, enums, casts, and actor-, component- and widget-class owners: 100,000 randomized cases each, zero mismatches, with the 11-invariant bind round trip and its proof-of-execution call counterWorld-time-derived outputs and functions drawing conclusions from a live level still need PIE in your project
Self-testsThe 7-case type-identity matrix and the 7-assertion registration-ownership suitePerforce checkout of generated files - the path exists but no Perforce workspace has exercised it
CookThe packaged plugin mounts and cooks inside a generated content-only host projectA full-game BuildCookRun, staging, and pak

Design

Nine decisions shape the plugin, and most of them exist because something was tried, measured, and found broken. Summaries below; the full record, failures included, ships with the plugin as DESIGN.md.

The emitter lowers what it can prove from the bytecode and refuses everything else by name, at generation. A refused function stays on the Blueprint VM and works exactly as before. The failure mode 'plausible-looking wrong C++' is designed out: an unknown opcode fails the decode, and no code is produced from a stream the decoder did not fully account for.

Generated code is derived output. The canonical source hash ties each generated body to the exact bytecode it was compiled from; editing the Blueprint breaks the match, the VM takes over at the next bind, and the status line says stale. Nothing you do in the Blueprint editor can be silently overridden by stale native code.

The hash covers opcodes, operands with pointers replaced by path names, jump targets as statement indices, the parameter and local layout, and every callee by path - with instrumentation dropped, FName-derived strings case-folded, and sizes excluded. The same function hashes identically in the editor, a commandlet, and a cooked build.

Blueprint struct member offsets are resolved at bind time from the live struct, never compiled into the generated code. Object literals are resolved by path and validate their assumed native ancestor. A struct edit or asset reparent fails the bind, and the VM keeps the function - silently reading wrong bytes is not an available outcome.

Randomized inputs run through both implementations; outputs are compared member by member and objects by content. The input pool keeps nulls, feeds interface implementers to functions that cast, and reports per-input branch coverage - because a green run that never took a branch proves nothing about it.

Bind moves the function's bytecode aside and installs the native body; Unbind restores it byte for byte. The editor unbinds around every package save, so 'native' can never be serialized into a .uasset. AutoBind 0 unbinds everything now, in both directions. ForceVM pins one function to the VM for this process without touching the manifest.

Every write goes through a file transaction with preflight, staging, atomic replace, and rollback - under a system-wide lock taken before the manifest is read. A commit journals its intent first, so a crash mid-write is recovered on the next run. The manifest is validated field by field and refused whole when malformed.

Delete Plugins/NativizedFunctions, or run bpnativizer.RemoveGenerated: every function returns to the VM and nothing else breaks. Remove fails closed - if the .uproject cannot be updated, nothing is copied and nothing is deleted.

The compatibility table on this page claims exactly what the release gates last proved, nothing wider. The gates are runnable: the self-test suites ship in the plugin, the sweep and parity harness run against your own content, and the README names what is NOT verified with the same prominence as what is.

Support

The README, CHANGELOG, and design record ship inside the plugin package. For everything else, use the channels below.