DevSnapshot: 6.2.0-beta.18
Write it in C#. Ship it to a Wii. Build it on a Mac. Beta 18 rolls up everything since beta 9 into one release, and it's the biggest one Polyphase has had. The animation system grew up: layered and masked playback, reusable clips, humanoid retargeting, and now bone sockets with animation notifies so the sword lands in the hand on the right frame. Scripting opened up: C# that runs on every console you already target, and a WebSocket client that works on a GameCube. Shipping got serious: streaming audio, a single-archive content format that hides your asset tree, Linux RPM and AppImage packaging, and a memory profiler that tells you whether you fit on the hardware before you burn a disc. Plus real spot lights on every backend, baked occlusion culling that runs on a GameCube, 3DS games that install as CIA, and an addon registry that has grown to a dozen more build targets and a shelf of gameplay, format and editor addons that never got a proper announcement. Until now.
And the editor runs on a Mac. As of this build, there's a download for it.
One housekeeping note. This post covers beta 16, 17 and 18. The tags landed a week apart and nobody needs three posts, so it's one.
If beta 9 was about authoring, this one is about who gets to build with Polyphase, and what they can ship.
View The Full 6.2.0 - beta 18 Changelog Here
The Short Version
If you read nothing else:
- The editor runs on a Mac, and you can download it. Apple Silicon, Vulkan over MoltenVK, and it builds for GameCube, Wii, 3DS and every addon console from the same machine. The
.dmgcomes out of CI with every release now. Games ship as signed.appbundles. - C# scripting. Roslyn to Lua ahead of time, so C# runs on every console the engine targets with no .NET runtime in the game.
- WebSockets on Wii and 3DS. A full client from Lua, with
wss://on desktop. - Baked occlusion culling. Unity-style potentially-visible sets, one bit test per draw, works on GX, C3D and PS2 as well as Vulkan.
- Spot lights on every backend, including the GameCube's hardware cutoff and a per-light LUT on 3DS.
- Bone sockets and animation notifies. Put the sword in the hand on the frame the hand reaches the hilt.
- Layered and masked animation, reusable clips, humanoid retargeting. Mixamo clip, custom rig, it works.
- Streaming audio and Content Pak. Multi-minute music on a 3DS, and a single obfuscated archive that hides your asset tree.
- A Memory Snapshot Profiler that tells you whether the level fits in 24 MB before you burn the disc.
- 3DS games install as
.ciawith a 3D animated banner and a tune, straight from the Packaging window. - Linux RPM and AppImage targets, and addons that update and uninstall themselves from the Addons Manager.
- A dozen more platforms as build-target addons: PS2, PSP, Dreamcast, PS3, N64, original Xbox, WebGL2, WebGPU, Android TV, Android Universal, Linux ARM64, plus a registry full of gameplay, format and editor addons, all catalogued below for the first time.
- Create a first- or third-person character from one dialog. Capsule, camera rig, controller script, all wired. Press Play.
- HTTP on Android. Same
HttpAPI as desktop, HTTPS through the phone's own certificate store. - Content packs you can download. Mount a second
Content.pakover the first, even straight out of memory on a console with nowhere to write. And the pak format works on a real PS2 now, not just PCSX2. - Run the editor as a service.
-servestarts the REST controller with no window and no GPU. - USB Gecko logging and GDB on GameCube and Wii. Plug it in and the log comes out. Build with
POLYPHASE_GDB=1and set breakpoints. - UI-only scenes render right. No camera in the scene? You get one, with a warning, instead of black bands.
Headline Features
The Editor Runs on a Mac
Apple Silicon. Full editor. Every console.
Beta 16 brings a native macOS build of the Polyphase editor and game runtime. Vulkan runs over MoltenVK from the LunarG SDK, the window is a Cocoa NSWindow with a CAMetalLayer, gamepads come through GameController.framework, audio through an AudioUnit, and Command is aliased onto Control so every shortcut you already know still works. Retina is handled in backing pixels end to end, and the editor picks its interface scale from the display.
It isn't a viewer. The whole toolchain came across: Bullet, Assimp, libgit2 and the shader compiler all build from the new Makefiles, native addons compile as .dylibs and hot-reload, the Lua editor and terminal work, and the Build Dependencies window knows about Xcode Command Line Tools and glslc. Packaging a game for Mac produces a real <Project>.app with MoltenVK bundled, an .icns generated from your PNG, ad-hoc signing at minimum because Apple Silicon refuses unsigned binaries, and optional Developer ID signing, notarization and a .dmg, all from profile options. Build & Run opens the bundle.
And because devkitPro ships macOS packages, a Mac can package for GameCube, Wii and 3DS natively, no WSL equivalent required. The setup guide walks through it. The build-target addons that already run their toolchains natively on Linux, like PSP and PS2, work the same way.
Beta 18 puts the .dmg on the release page. The release workflow grew a build-mac job on an Apple Silicon runner. It installs the Vulkan SDK from our own mirror first (LunarG rotates old versions off their CDN, and old releases should stay reproducible), builds the editor, the game runtime and the libLua.a that native addons link against, wraps it into Polyphase.app, verifies the disk image, and uploads PolyphaseEditor-<version>-macos-arm64.dmg next to the Windows and Linux builds. The bundle is ad-hoc signed, not notarized, so the first launch is a right-click and Open. Developer ID signing is two secrets away and not wired yet.
Beta 18 also fixed the input bugs beta 16 shipped with, which anyone who tried it hit inside a minute. Cmd+C, Cmd+V and Cmd+A did nothing in text fields, because Command was aliased onto Control for the editor's hotkeys and ImGui's own shortcut matching wanted the real modifier set. Holding a key typed one character. A native file dialog, a fullscreen toggle or a Cmd+Tab could leave the editor deaf to the keyboard until you clicked around. All three are gone, and along the way every editor shortcut and every Ctrl-click multi-select was routed through one shared modifier helper, so Cmd is the shortcut key everywhere on a Mac and Ctrl still means Ctrl in the terminal.
Limits, stated up front: Apple Silicon only for now, no Intel or universal binaries; the compute light baker stays on Windows and Linux; Metal has no wide lines so debug lines are one pixel. Everything else you'd do on Linux, you can now do on a MacBook.
See Documentation/Development/SetupEnvironment/Mac.md and Documentation/Development/Platforms/Mac/.
C# Scripting
If you came from Unity, you can stop translating in your head.
Polyphase scripts can now be written in C#. Derive from Script3D, mark up your fields, override the lifecycle methods you care about, and hit Ctrl+R.
using Polyphase;
public class Rotator : Script3D
{
[Property] public Vector3 AngularVelocity = new Vector3(0, 90, 0);
[Property(Display = "Spin Enabled")] public bool Enabled = true;
public override void Tick(float deltaTime)
{
if (Enabled) AddRotation(AngularVelocity * deltaTime);
}
}
Here's the trick that makes it work everywhere: there is no .NET runtime in your game. Your C# is compiled by Roslyn, transpiled to ordinary Polyphase Lua, and runs on the same Lua VM the engine already ships on Windows, Linux, Android, Web, PS2, PSP, 3DS, Wii, GameCube and Dreamcast. A C# script on a 3DS is just a Lua script the 3DS never knew was C#. The .lua sits next to the .cs, so teammates who never touch C# never need the .NET SDK installed.
[Property] fields show up in the inspector with the right type and a tooltip. [Button] methods become inspector buttons. Public methods are callable from Lua and from other C# scripts. Save the .cs and the editor transpiles and hot-reloads it for you. Visual Studio or VS Code opens with full IntelliSense against the engine API through Tools > CSharp > Open C# Solution.
You get Script, Script3D, Node3D, Camera3D, SkeletalMesh3D, World, Vector3, Color, Input, Mathf, Log, and a Lua escape hatch for anything not yet typed. It's a v1 and we're honest about the edges: the Lua VM is 32-bit so long and decimal are out, no async, no reflection, no binary NuGet packages , vendor source instead. But dotnet is the only thing you need to install, and if you don't have it the editor tells you exactly how to get it.
See Documentation/Info/CSharpScripting.md.
WebSockets: Yes, on Wii and 3DS
Leaderboards, chat, live events, a relay server for your multiplayer prototype. From Lua. On the same consoles you already ship to.
self.ws = WebSocket.Connect("ws://192.168.1.10:9002", { protocols = { "game.v1" } })
self.ws:SetMessageCallback(function(data, isBinary)
Log.Debug("server said: " .. data)
end)
self.ws:SetOpenCallback(function() self.ws:SendText("hello") end)
A full RFC 6455 client, callbacks or polling, pumped once a frame with no threads in the core , because your consoles build with no exceptions, no RTTI and no threads, and it had to work there. ws:// runs on Windows, Linux, Android, 3DS, Wii and GameCube. wss:// runs on Windows via WinHTTP and on Linux via libcurl. Web builds get it through the browser. Everything's in the 1 MiB / 16 MiB queue limits by default, subprotocol negotiation works, ping is handled for you, and dropping the connection handle closes it cleanly.
Along the way the Wii and GameCube network stack got fixed properly. Connects were being reported as failures because IOS answers asynchronously, socket errors were sneaking through as valid handles, and a missing default gateway now warns instead of leaving you staring at a timeout.
There's a dependency-free Node echo server in Tools/ws-echo/ so you can test without standing up infrastructure.
Baked Occlusion Culling
The room behind the wall costs nothing now. On a GameCube.
Flag your walls as Occluder Static, your props as Occludee Static (or select everything and hit World > Mark Selected Occluder + Occludee), tick Occlusion Culling on the scene, and run World > Bake Occlusion Culling. The baker divides the level into cells, fires rays from sample points in every cell against a BVH of the occluder geometry, and records which occludees can be seen from where. Identical visibility sets are pooled, cell indices shrink to 16 bits when they fit, and the whole thing serializes into the scene.
At runtime it's one cell lookup and one bit test per draw, before frustum culling even runs, on the CPU. That's the point: it works on Vulkan, on GX for Wii and GameCube, on C3D for 3DS, and on PS2 and PSP through the addon targets, with no per-backend code. Culled meshes still cast shadows, and the editor viewport never culls, so you can't lose an object you're trying to select.
Moving objects get a second, coarser table so a character that walks behind a wall still culls. Re-bakes are incremental, reusing every cell whose occluders didn't change. If you move an occluder after baking, a toast tells you the data is stale and offers a one-click re-bake. Quality presets go from four samples per cell to seventeen, and a Console Budget in kilobytes strips the table from a console cook if it would blow the memory you set aside, with a warning instead of a silent overrun.
There's a View > Occlusion Preview overlay that draws the volume, the game camera's cell, and every occludee green or red, a Culling mode on the stats overlay, REST routes to bake and query from outside the editor, and a small Lua and C++ API. Plus World:IsSeenByCamera(node) and World:GetSeenByCamera(tags), which reuse the same data to answer "can the player see this right now?" for gameplay, with no extra raycasts.
See Documentation/Development/OcclusionCulling.md.
Spot Lights, Every Backend
A new SpotLight3D node with inner and outer cone angles. Point it with the gizmo or SetDirection. It's a PointLight3D underneath, so radius, domain, culling and everything you already knew still applies.
- Vulkan gets a smooth inner-to-outer falloff in both forward shaders.
- Lightmap baking understands spot cones, shadows and all.
- Wii and GameCube use the hardware
GX_InitLightSpotcutoff. - 3DS builds a per-light spot LUT and regenerates it only when the angles change.
- Backends without cone support render it as a point light rather than dropping it.
Importing a scene with spot lights in it brings them across. Ctrl+Alt+2 spawns one.
And a small thing that matters if you target consoles: the editor viewport can now preview console lighting. When your active build target is a GameCube, Wii, 3DS, PSP or PS2, the desktop renderer clamps light intensity the way fixed-function hardware will, so what you see in the editor is what you see on the TV.
Bone Sockets and Animation Notifies
Draw the sword when the hand reaches the hilt. Not when the animation starts.
A socket is a named point on a rig , a bone plus a constant offset, authored once on the SkeletalMesh asset. Every prop attached to a socket lands identically. The workflow is three right-clicks: attach a prop to a bone, nudge it into the palm with the gizmo, Create Socket From Selection. Now WeaponHand_R exists for every character sharing that rig.
function Player:Start()
self.sword:AttachToSocket(self.mesh, "Sheath_Back")
self.mesh:SetAnimationUpdateMode(AnimationUpdateMode.AlwaysUpdateTime)
self.mesh:AddAnimationNotify("DrawSword", 0.4, function()
self.sword:SetAttachSocket("WeaponHand_R")
end)
end
AddAnimationNotify fires once each time playback crosses a normalized point. It handles looping clips, reverse playback, timeline scrubbing and frames long enough to step over the threshold , all the cases the old Event_* bone system silently missed, and with no Blender re-export. GetSocketPosition("Muzzle") gives you a world position for spawning a flash without parenting anything.
This also fixes the bug that made bone attachment nearly unusable: dragging a bone-attached prop with the gizmo multiplied its scale every frame, 100× on Mixamo rigs. Three separate transform bugs in that path are gone.
Sockets are name-keyed and survive a reimport. See Documentation/Development/BoneSockets.md.
Layered and Masked Animation
Run with your legs and punch with your upper body. No combined clip required.
Every SkeletalMesh3D now has eight animation slots, each with its own weight, speed, bone mask, and layer mode. A mask is a BoneMaskAsset , you pick include roots and exclude roots by bone name, and the mask resolves against whatever rig asks for it. Name-keyed, so one UpperBody mask drives every character that shares those bone names.
-- Lower body: full-body locomotion, cross-faded
mesh:PlayAnimation("Idle", true, 1, 1.0 - runAlpha, 0)
mesh:PlayAnimation("Run", true, 1, runAlpha, 1)
-- Upper body: masked attack, plays once and self-removes
mesh:PlayAnimationMasked("Punch", 2, UpperBodyMask, false, 1.0, 1.0)
-- Hit reaction on top of everything, additive
mesh:PlayAnimationAdditive("HitReact_add", 3, UpperBodyMask, false, 1.0, 1.0)
Slots blend in order. AnimLayerMode.Replace interpolates toward the clip by weight; AnimLayerMode.Additive adds the clip's offset from its bind pose on top of whatever the lower slots produced. FadeSlotWeight ramps a slot in or out over seconds, so you can blend an aim pose in without a state machine.
All of this happens on decomposed local transforms before the parent-chain walk, which means it works on every backend , Vulkan, GX on Wii and GameCube, C3D on 3DS, and addon targets , with no per-platform code.
Masks get a real editor. View > Bone Mask Editor opens a dock with its own preview world: orbit camera, animation picker, play and scrub, and the included bones drawn as green segments over the skeleton so you can see exactly what your mask covers before you ship it.
See Documentation/Development/BoneMask.md for the full guide.
Reusable Animation Clips
Animations no longer live trapped inside the mesh they were imported with. SkeletalAnimationAsset is a first-class clip you can extract, copy, share, and bind to any compatible rig , channels are keyed by bone name, not bone index, which is what makes one clip work across rigs.
- Right-click any SkeletalMesh → Extract Animations... Pick which embedded clips to pull out, get standalone assets you can browse and reuse.
- Import Animations in the asset browser reads animation-only
.fbx/.glb/.gltf/.daefiles that carry no render mesh at all. Point it at a folder of Mixamo downloads and mass-import a library. - Assign clips to a
SkeletalMesh3D's Animation Assets vector in the inspector.PlayAnimation("Walk", true)resolves through the mesh's embedded chain first, then your external library.
Humanoid Retargeting
Mixamo clip, custom character, different bone names, different proportions. It works.
HumanoidAvatarAsset maps 21 humanoid slots onto a rig. Auto-map from Reference does the first pass for you and understands Mixamo, Rigify, Auto-Rig Pro, and Blender .L / .R suffix conventions out of the box , the slots it can't guess are flagged in the inspector so you know exactly what's left to fill in.
Then right-click a clip → Retarget... and pick your source and target avatars. Tier 1 is a straight name remap. Tier 2 is the one you want:
worldDelta = sourceWorldRot * inverse(sourceWorldBindRot) -- what the animation did, in world space
targetWorld = worldDelta * targetWorldBindRot -- apply the same delta to the target's rest pose
targetLocal = inverse(targetParentWorldRot) * targetWorld -- back to local space
Because everything is computed in a common frame, bone-axis conventions drop out of the math. That's the fix for the classic "arms come out backwards" failure that local-bind retargeting can't solve. Hips translation scales by the bind-pose height ratio, so a short character doesn't skate.
Honest about the limits: there's no IK end-effector solving, no twist-bone distribution, and no synthesis for joints your target rig doesn't have. Feet will slide on a proportion mismatch that only IK could fix.
See Documentation/Development/SkeletalAnimation.md.
Multi-Part Characters, Per-Section Materials
Import a skinned GLB with body, head, clothes, and accessories as a single object and every part keeps its own material slot. The asset inspector lists each section with a per-row material picker and a triangle count, and SkeletalMesh3D adds per-instance overrides , so you can recolor one hero's cape in one scene without touching the shared asset.
node:SetMaterialSlot("Cape", LoadAsset("M_Cape_Blue"))
node:SetMaterialSlot(1, LoadAsset("M_Body_Tanned"))
This also fixes a long-standing import bug: the combined importer used to pick the first skinned primitive as canonical and silently drop weights for bones that only appeared on later ones. Accessories and clothing that animated correctly in Blender arrived rigidly stuck to the body. The importer now builds a bone superset across every primitive.
Renders one draw per section on Vulkan, GX, and C3D.
Streaming Audio
Tick Streaming on a SoundWave and the shipped game stops loading that clip into RAM. The .oct records where the PCM lives and the audio manager feeds it to the platform's stream voice in chunks. That's what makes a multi-minute music track viable on a console with a few megabytes to its name.
One thing to know up front: the checkbox does nothing in the editor, deliberately. Packaging re-serializes the in-memory SoundWave, so if the editor skipped loading the audio it would write an .oct with zero PCM bytes and ship you a silent game. Editor sessions always load resident; the flag only changes behavior in a built game.
Chunk sizes are tuned per target. Console addon builds read 128 KB at a time , roughly one read every six seconds of audio , because on slow media each blocking read carries a large fixed command latency and you want those rare. Desktop I/O is fast and stays at 16 KB. Streaming voices are 2D; a streaming wave played through PlaySound3D routes to the 2D path.
If a platform has no stream backend, it logs and falls silent rather than faulting.
Content Pak and Static Content
Stop shipping your Lua source in plaintext.
Two new checkboxes in the build profile. Static Content ships cooked assets, scripts, and the asset registry obfuscated and decodes them on demand. Content Pak goes further and folds the whole lot into a single Content.pak , so the filenames and the folder tree are hidden too, not just the file contents, and your console target doesn't spend a directory entry per asset.
Only the pak index stays resident, about 10 to 20 KB for a few hundred entries. Everything else is read and decoded on demand, so console memory behaviour doesn't change. Lookups are a binary search on a path hash, confirmed against the stored path, so a hash collision is a miss and never the wrong asset.
It works everywhere, including out-of-tree build targets, because the whole thing hangs off two shared chokepoints rather than per-platform code. Decoding is endian-independent, offset-addressable so mid-file seeks and truncated reads still work, and allocates nothing , which is what lets streaming audio seek inside an obfuscated pak entry on a console.
Be clear-eyed about what this is: it's obfuscation, not DRM. The key is a compile-time constant and it ships inside your executable. It stops someone opening Player.lua in Notepad, hex-editing a .oct, and ripping your art. It does not stop someone with a disassembler. Don't tell your players otherwise.
Worth turning on even for embedded builds , arguably especially there. Without it, an embedded build carries your Lua source and every asset path as plaintext in the binary, and strings game.dol finds it immediately.
See Documentation/Development/StaticContent.md.
Memory Snapshot Profiler
Will it fit on a 3DS? Now you know before you build.
Tools > Memory Snapshot captures the running game , not the editor , and attributes every byte to the asset that owns it. Textures are sized by their real pixel format with mips, CMPR compression and console downsampling accounted for. Streaming audio counts as zero resident bytes because that's what it is. Skeletal meshes split into geometry and animation keyframes. Lua source, fonts, particles, materials and frame buffers all get their own rows.
Pick a RAM budget from seventeen console presets , 2 MB up to 32 GB, from an N64 to a workstation , and a bar turns green, amber or red. Sort the table by footprint, find the 4K texture someone left on a doorknob, and fix it. Snapshots save to a Debug/ folder beside your assets with a thumbnail of what was on screen, so you can compare a level across builds.
The Profiling window now reads from the same numbers, so the two views can never disagree.
Linux RPM and AppImage Packaging
Two new build targets under Desktop: Linux (RPM) and Linux (AppImage). Pick one, fill in a version and an icon, and packaging hands you an installable .rpm or a single-file .AppImage that runs on any distro without FUSE. On Windows the whole thing runs through WSL for you , path translation, tool detection, the lot. The AppImage mount is read-only, so saves now fall back to ~/.local/share/<Project>/Saves automatically.
Build-target variants now key the build cache correctly, too. RPM, AppImage, PSP, Dreamcast and PS2 all share the Linux base platform and used to share one manifest, which meant switching between them could report "up to date" and skip the post-package step entirely. Each target gets its own manifest now.
Fill Anchors and Flex Layout
Three new anchor modes , Fill, FillHorizontal, FillVertical , and an ArrayWidget that knows what to do with them.
A Fill child ignores its offset and size on the filled axis and takes whatever the parent gives it. Drop a background behind your menu with bg:SetAnchorMode(AnchorMode.Fill) and nothing else. Build a full-width 40px header with FillHorizontal plus SetSize(0, 40), or a full-height 200px sidebar with FillVertical plus SetSize(200, 0).
Inside an ArrayWidget they become flex-grow. Fixed children keep their size, Fill children split the leftover space evenly, and the four new padding properties inset the whole thing , so a fixed toolbar, a growing content area, and a fixed footer is one array widget and three children instead of a pile of nested wrappers and hand-computed offsets.
The XML UI path speaks it too, via anchor="fill", "fill-horizontal" and "fill-vertical".
Create a Character in One Dialog
Tools > Create > First Person Character. Name it, pick a mesh, hit Create, press Play.
Every project starts the same way: a capsule, a camera, the controller script, and twenty minutes of getting the eye height right. That's a dialog now. Pick First Person or Third Person, give it a name, optionally pick a SkeletalMesh and a height, choose a script (the built-in FirstPersonController and ThirdPersonController are listed, so is every script in your project, and you can drag one in from the Scripts panel), and decide whether it gets spawned into the open scene or saved as a Scene asset you can drop into every level.
What you get is a Capsule3D with collision on, overlaps on, physics off and a Player tag, because the controllers move by sweeping, not as a rigid body. First person puts a Camera3D at eye height. Third person adds a CameraPivot at chest height with the camera behind it. If you picked a mesh it stands with its feet on the capsule bottom, scaled to your height if you asked, and if the rig has an Idle clip that becomes the default animation. A Controller node carries the script with collider, camera, cameraPivot and mesh already filled in, so renaming a node later doesn't break anything. Every dimension scales from the one height you typed: radius, eye line, pivot, follow distance. A 1.8 m character and a 0.5 m character both come out right.
It's one undo step. The rig is built and checked before anything touches the scene, so a script that fails to load leaves you with an error message, not half a character.
HTTP on Android
The same Http API you use on desktop, now on the phone.
Android was the one desktop-class platform with no HTTP client. It has one now, and it goes through the OS: HttpURLConnection over JNI, which means HTTPS uses Android's own certificate store and stays current with the system, the same way we use WinHTTP on Windows and httpc on 3DS rather than shipping our own OpenSSL. Headers, POST and PUT bodies, timeouts, redirects (including across http and https, which the platform's built-in follower refuses to do), error bodies on a 4xx, cancellation, and the same 64 MB body cap as everywhere else. VerifySsl(false) turns off checking for that one connection and nothing else.
What it doesn't do yet: stream a download to disk, report progress, or use a proxy. Bodies land in memory.
Content Packs You Can Download
Mount a second Content.pak on top of the first. From disk, or from a buffer you just pulled off the network.
Beta 16's Content Pak was one archive, mounted once. It's a stack now. Mount another pak and it sits on top: lookups check the newest first, so a downloaded bundle can override a level, a texture or a script in the base game without touching it. Unmount it and the base comes back. MountMemory mounts a pak straight from RAM, which is the only option on a GameCube or a Wii where the memory card is the whole writable world. The pieces an addon needs to build a real DLC or asset-streaming flow are exported too: fold a bundle's asset registry into the running one, register an asset whose path lives inside a mounted pak, and let Stream::ReadFile sort out where the bytes are.
Then we ran it on a real PlayStation 2 and it said "not a valid pak". PCSX2 never did. On hardware every file read is a remote call to the IOP through one shared buffer, the audio mixer thread is using that same buffer every couple of milliseconds, and the disc driver serves an unaligned read as three. The index came back scrambled. Pak reads now go through a sector-aligned bounce buffer, take the SIF lock per request, are sliced so the mixer is never starved, and the index read is retried against its own checksum. Streaming file reads got the same lock and an 8 KB slice, which also killed the double-free crash inside _request_end. The fix lives in the shared path, so every platform reads paks the same way now.
If you wrote addon code against the old ContentPak::Mount and Unmount, the signatures changed. Mount returns a handle and Unmount takes one.
Run the Editor as a Service
PolyphaseEditor -serve -project MyGame/MyGame.octp and the REST controller is up. No window, no GPU.
Headless mode used to mean one thing: cook and exit. -serve keeps the process alive with the controller server listening (port 7890 by default, or pass one), native addons loaded so their node types resolve, the project auto-upgraded, and addon-registered REST routes live. That's what a build box, a CI job or an asset pipeline wants: import a mesh, retarget an animation, query a scene, cook a target, over HTTP, for as long as the process is up.
Getting there flushed out every place the editor assumed it had a window. On a Linux box with no display or no ALSA device, headless would trip an ImGui assert in the theme loader, dereference a null XCB connection in the event pump, hand nullptr to snd_pcm_avail, and run the full render pass against a Vulkan context that didn't exist. All guarded. Import fixup prompts that would have waited forever for a click now pick the non-destructive answer and log that they did.
Debug a GameCube Over a USB Gecko
Plug the Gecko into slot B and the log comes out. Build with POLYPHASE_GDB=1 and set a breakpoint in engine startup.
Console logging on Dolphin builds went wherever SYS_Report pointed, which on a real Wii is an EXI UART nobody owns. The engine now probes both memory card slots for a USB Gecko at boot, before video init, and mirrors every log line to it. Non-blocking, so an unplugged cable or a closed terminal never stalls the game. The same change fixed a real bug: SYS_Log passed the formatted line back in as a format string, so a stray % in a log message got expanded twice.
For source-level debugging, make POLYPHASE_GDB=1 builds with -Og -gdwarf-4, links libogc's debug stub, and calls DEBUG_Init plus _break() as the first thing the game does, before the config is even read. Point powerpc-eabi-gdb at the ELF and target remote at the COM port. Two things to know: devkitPPC's GDB is 7.6.2 and can't read the DWARF-5 that modern GCC emits, which is why the flag forces DWARF-4; and objects don't track the flag, so make clean when you toggle it. The Gecko log is compiled out in GDB builds because the stub owns that channel.
Build Targets: A Dozen More Platforms From One Editor
The engine ships six built-in targets: Windows, Linux, Android, GameCube, Wii and 3DS. Everything else is an addon you install from the registry, and the list has quietly grown to a size we've never actually written down in one place. Here it is.
Retro Consoles
PlayStation 2. The most complete of the console addons. Textured and vertex-colored static meshes, CPU-skinned skeletal meshes, instanced meshes, particles, tilemaps, terrain, voxels, 3D text, the full widget UI with font atlas. CPU per-vertex lighting with ambient, one directional and four point lights, and as of this week, spot lights. GS hardware fog with a horizon-gradient skybox. An eight-voice plus four-stream software mixer feeding audsrv. DualShock 2 with pressure and rumble. Memory-card saves with a proper icon.sys. Masked blend mode and translucent depth sorting landed in beta 14. Builds a bare ELF or an ISO for PCSX2 and FreeMcBoot, natively on Windows or Linux. Thirty-two megabytes of RAM and a non-IEEE FPU, and it runs.
PlayStation Portable. Nineteen commits deep and the first console addon to get Content Pak support, so your PSP game ships as one obfuscated archive. Fixed-function sceGu rendering with hardware vertex lights, an sceAudio mixer thread, save data that associates with the XMB, particles, CPU-skinned characters, out-of-tree builds. Packs an EBOOT.PBP with your own ICON0 and PIC1, launches in PPSSPP or deploys over pspsh. Two megabytes of VRAM and it still looks like a game.
Dreamcast. Loads scenes, renders static and skeletal meshes on the PowerVR2, draws the UI, plays audio of any length through an optimized streaming path, and lights with point lights. Three toolchain routes so nobody is stuck: DreamSDK on Windows, a native KallistiOS install, or KallistiOS under WSL. Outputs a .cdi or .gdi through mkdcdisc, plus a selfboot folder for mastering a retail-BIOS disc. Runs in flycast, lxdream or redream, or deploys to hardware over dc-tool-ip.
PlayStation 3. Boots in RPCS3, loads scenes, renders meshes and fog on the RSX with Cg shaders, draws UI. Fake-signs into a bootable .self and wraps it into an installable .pkg. Still early: memory work is ongoing and script support is being stabilized.
Nintendo 64. Packages to a ROM through libdragon and renders lit static meshes on the RDP. Textures are the next milestone. If you ever wanted to see your Polyphase scene on a console from 1996, that's now a build profile.
Original Xbox. Boots under nxdk, loads scenes with directional lighting, static meshes and textures on the NV2A. Geometry is still being straightened out. One commit old and already drawing.
Nintendo DS. Scaffolded. The package exists so the registry knows about it; the runtime hasn't started.
Web
WebGL2 and WebGPU. Your game as a single-threaded WebAssembly module that runs from any static host, no SharedArrayBuffer or special headers required. Static and CPU-skinned meshes, particles, UI, fog, ambient plus directional plus point lights, OpenAL audio that starts on the first click, keyboard, mouse with pointer lock, touch and gamepads. HTTP goes through fetch. Multiplayer tunnels the engine's NET_* calls over a WebSocket to a tiny Node relay that ships with the addon. The WebGPU flavor uses Dawn and WGSL ubershaders, handles eight point lights, and coexists with the WebGL2 target in the same project. Build & Run starts a local server and opens your browser.
Android and ARM
Android TV. Stages the engine's Android project, applies a Leanback manifest overlay with a TV banner, patches Gradle, builds the APK and installs it on the box over adb. Android Universal does the same without the TV bits and builds arm64-v8a and armeabi-v7a into one APK, which is the fix for every cheap tablet that refuses to install a 64-bit-only package. Linux ARM64 cross-compiles the Vulkan runtime against a Debian sysroot for Raspberry Pi 4 and 5, Orange Pi 5 and friends, with a script that builds the sysroot for you.
And One That Doesn't Compile Anything
Project ZIP Archive zips the open project, honoring .gitignore, into Packaged/. No toolchain, no external tools. It's the thing you reach for when someone says "send me the project."
Every one of these is a native addon written against the same build-target API you can use for your own hardware. The PS2 addon picked up spot lights the same week the engine did, and the PSP addon picked up Content Pak the month it landed, which is the whole point of the design.
Addons: Systems, Formats and Tools 🧩
Beyond build targets, the official registry now carries gameplay frameworks, media formats and editor tooling. Install from Addons Manager > Browse, which reads one auto-generated manifest per registry and groups everything by category. You can run your own registry for a studio or a team; the repo has a guide for that.
Gameplay Systems
FPS Framework. A complete first-person layer: input bindings for three fire modes, reload, weapon swap, pause and Z-lock; navmesh-driven enemy AI with five archetypes and tunable sight, accuracy and speed; six weapon archetypes; missions with story briefs; main, pause and settings menus. It sits on top of Combat Core and Mission Core, and the reference player, weapon and enemy scripts copy into your project so you can rewrite them.
Mission Core. Genre-neutral. A runtime mission registry plus editor-authored MissionAssets with objectives, a briefing, the scene to load and a weapon loadout, and a Lua flow orchestrator for pick, accept, begin and cancel. Every state change goes out over the SignalBus. FPS, RPG and point-and-click addons build on it.
Items Core and RPG Loot. ItemInfo assets authored in the inspector with icon, mesh, category and per-category effect parameters. LootTable assets with guaranteed and weighted entries, min and max counts and condition tags, plus a dockable Loot Table Editor window. Lua composes overlays on top of the authored data, rolls, and either spawns world pickups or applies rewards directly.
Dialogue Core. Branching conversations with choices, variables, events and save/load, a DialogueRunner3D node, a DialogueBoxWidget with portraits and press-to-continue, and an editor preview that steps through a graph without placing it in a scene. Console-safe by construction: no RTTI, no exceptions. The Lanterns and Lies demo game is built on it.
World Stream. Open worlds on a GameCube. Sectors load when a source enters their radius and unload after a delay, with console-safe defaults of one load and one unload per frame and two active sectors. Distance or trigger-volume modes, always-resident sectors, a Lua API, and a View > World Stream window showing every sector's state and distance.
Formats
MIDI and SoundFonts. Play .mid files through .sf2 banks with an embedded TinySoundFont synth, generate music at runtime from Lua, and edit in a piano roll under View > MIDI. Runs on Windows, Linux, 3DS, Wii and GameCube with per-console voice caps, so a chiptune soundtrack costs kilobytes instead of megabytes of PCM.
Webcam. Live camera feed as a Texture on a quad, a material or a widget. Media Foundation on Windows, V4L2 on Linux, and the 3DS's own inner and outer cameras through libctru, verified on retail hardware. GrabFrame() returns a stable snapshot texture for photobooth mechanics.
Video Player. Since beta 9 it has grown Wii, GameCube and 3DS playback through built-in software decoders and a cook step with Tiny, Balanced and Quality presets. Audio is the master clock, so loop and sync behave the same everywhere.
Gaussian Splatting. Import .ply and .splat captures, drop a GaussianSplat3D node in the scene, render on Vulkan. A new compute-based tile rasterizer landed this week alongside the original splat renderer, with the preprocess, prefix-sum, scatter, render and composite passes you'd expect. Consoles load the metadata and no-op the renderer. Still v0.1, and the repo is private for now.
Editor Tools
Level Builder. A dockable Level Builder window with a shared framework for tools, brushes, snap providers, palettes and preview, plus a Kit Editor for authoring kits without leaving the editor. Two placement modes: Modular for snap-based kit building, where walls, floors, doors and props click together socket-to-socket, rotate on the R key, and every socket gets a three-axis gizmo you can drag in the viewport; and Grid for uniform-grid block placement, Minecraft-style. Kits are JSON folders, but drop a Kenney asset pack under Kits/ and it's recognized automatically, and .kit zip bundles can be shared between projects. A generic brush pack adds Line, Box, Box Fill, Noise Fill, Replace, Paint and Mask Fill brushes that work in either mode.
Houdini Engine and HDA. Load Houdini Digital Assets in the editor, feed them static meshes, node groups and Spline3D curves, edit parameters, cook through Houdini Engine, and bake the result into ordinary StaticMesh assets, StaticMesh3D nodes and splines. Point clouds collapse into InstancedMesh3D. Nothing Houdini-specific ships in your build. Windows, Houdini 20 or 21, Engine or full license.
Platform Wins
3DS Games Install as CIA
A second 3DS target, Nintendo 3DS (CIA), sits beside the .3dsx one. It shares the cook and the compile, then runs makerom in the post-package step and hands you an installable .cia for FBI or Azahar. The reason to care: a CIA gets the full application memory region, 64 MB on an old 3DS and 124 MB on a New 3DS, where a homebrew launcher title gets a fraction of that.
The Packaging window gained the whole HOME Menu presence. Title, description, author and icon go into the SMDH. Product code, unique ID and version go into the RSF, with sane defaults derived from your project name. The banner can be a 256x128 image or a 3D scene: pick a scene, and the exporter writes the visible static meshes to glTF with your lighting, converts them to CGFX through pycgfx, and animates a spin with the keyframe timing the HOME Menu insists on. Add a two-second tune and it plays when the player hovers your icon. A Download button fetches makerom, bannertool and cwavtool at pinned versions, since devkitPro ships none of them.
The same SMDH options apply to the .3dsx target too, so both builds get a proper icon.
Consoles Boot Clean
Deploying a console build into a per-title folder on an SD card used to produce a game with no default textures, no default materials, and no fonts , so no UI. Engine content is expected at Engine/ relative to the working directory, but a per-project deploy puts it inside the project folder while the working directory stays at the card root. That's resolved now, and embedded builds are no longer the only ones that work.
Along with it: packaged builds stage the project file and Config.ini where the standalone runtime actually looks for them; addon-target consoles get the asset registry forced on like the built-in ones always did (on PSP that wasn't just a speed issue , interleaved directory reads silently dropped entries, so some assets never registered at all); PS3 stopped faulting on scanset parsing in Config.ini; and textures below 8x8 no longer cook to solid black on 3DS.
Script Hot-Reload Works on the Linux Editor
It never has. The file watcher was Windows-only , it logged "not implemented for this platform" and returned false everywhere else, so no amount of toggling the preference would have helped you. It's now a portable polling watcher that behaves the same on Windows, Linux, and anything added later.
The rewrite took two real bugs with it. Watches used to stack up on every project switch and keep reporting paths from projects you'd closed. And the old Windows path stored its in-flight I/O buffers inside a vector that could reallocate out from under the kernel , which fired for real on any project with an addon Scripts/ directory.
3DS Fog
The 3DS fog LUT was built wrong: the first sample was forced to zero, which made the nearest depth read as fully fogged and corrupted the entire curve, and the delta half of the table was mis-indexed. The exponential path had been dead-coded out behind an if (false) with a comment admitting nobody understood the LUT format. Both are fixed, and exponential fog on 3DS now matches the desktop approximation.
Fog also reaches the skybox now. Distance fog is meaningless up there , the sky sits past the far plane and would flood the whole dome , so sky materials fade by the view ray's elevation instead: full fog at the horizon, clearing toward the zenith. Fogged distant terrain runs seamlessly into the sky instead of stopping at a hard line.
3DS Top and Bottom Screens Author in the Editor
Target Screen only ever worked on the hardware, through separate worlds. Author a scene in the editor with a bottom-screen subtree and you'd get an empty bottom screen, cameras that reported the wrong screen, and lights from both screens lighting both , the doubled diffuse blew the fixed-function backends out to white. The renderer now resolves screen membership per subtree, picks the right camera per pass, filters lights the same way, and the editor's second-screen preview uses the identical lookup. Node:SetTargetScreen and a TargetScreen enum expose it to Lua.
Large textures that didn't fit in the 3DS's roughly 6 MB of VRAM also used to draw black with no message. They now retry on the linear heap and tell you what happened.
UI-Only Scenes Render Right
A scene with no Camera3D in it, which describes every main menu, took a different path through the renderer: no scene pass, no post-process, no full-window paint. The UI came out offset into a corner with black bands, worst on Android where nothing masked it. The world now spawns a transient fallback camera when a scene has none, logs a warning telling you to add one, and hands the active slot to a real camera the moment one shows up. On top of that the UI pass sets its own scissor rect, which it never did, so with Resolution Scale below 1.0 the UI was clipped to the smaller scene rect until some widget happened to reset it.
Editor Wins
Addons Update and Uninstall Themselves
The Addons Manager grew the other half of package management. Check for Updates asks GitHub for the branch head of every installed addon and compares it against the commit recorded at install time, so it knows the difference between "new commits", "new version", "pinned to a commit" and "installed from a local folder". Update All does the lot. Remove walks the dependency graph the right way round: transitive dependents go first, local-only dependents are left alone with a note, and if a native addon is loaded the removal happens inside a project restart so no DLL is ever deleted out from under the editor. Every install, update and check now runs under the progress bar instead of freezing the window on a synchronous download.
Native addon builds also got a preflight. Missing dependencies, missing engine headers, a missing Vulkan SDK or a missing engine import library are caught before the compiler runs, and the failure modal names the problem, offers a How to fix section, and puts an Install
Imports Batch Under One Progress Bar
Drop forty textures onto the editor and they import at end of frame under a single cancellable Importing Assets modal instead of stalling the frame per file. Non-standard WAV sample rates get a fixup modal that offers to resample to 44.1 kHz in place, which matters because Android hard-locks 44.1 and every console cook assumes it.
Multi-Select Everywhere
Select ten nodes in the hierarchy and drag them to a new parent. Move them up and down as a block. Delete them in one undo step. Select a dozen assets and drag them to a folder, save them, duplicate them. Ctrl-click scripts in the Script Panel and delete or open them together. It should have always worked like this.
Linux Editor Plays Standalone
Play In Standalone on the Linux editor used to build a Windows package and then fail to run it. It now builds and launches a Linux .elf, from the packaged directory so Config.ini and assets resolve. Any desktop build launched from the editor goes through the same path now, and refuses politely if you ask a Windows host to run a Linux build.
Smaller But Lovely
Loose files multi-select in the asset browser. Ctrl and Shift select, type-coloured rows, and
Packaging (N files)/Delete (N files)context actions.Keyboard-to-gamepad emulation has an off switch in the Input Map window, and its stick Y axis is no longer inverted.
The SoundWave inspector shows duration and PCM size, and warns on a non-standard sample rate.
Headless builds take a target id.
-build polyphase.n3ds.cia embeddedreaches the post-package step, loads the matching build profile, and honours static content and Content Pak.Occlusion Area nodes bound the bake volume when your level is bigger than its occludees.
require()works in packaged builds. A custom searcher resolves embedded scripts, then the project, then the engine, Content.pak included. Loose files on disk still win in the editor.Reloading scripts no longer restarts the debugger. Boot scripts like
StartLuaPandaare excluded from hot reload, so Ctrl+R stops stealing the Lua hook mid-session.DatumType.Function, a script property that shows up as an inspector button and calls the method of the same name. Properties can carry atooltipnow, too.Third-person controller takes animation names as properties. A rig with
Sprintinstead ofRunno longer needs a copy of the script.Spot Light in the quick-spawn menu, with its own hierarchy icon.
Create Project handles missing parent folders. Pointing it at a path that doesn't exist yet just works.
Addon startup is quiet. The shadow-copy step was trying to
mkdiryour drive root every launch and logging a permission error for its trouble.Create Asset is categorized. Eight submenus , Scene, Material, Animation, Tilemap, VFX, UI, Input, Logic , instead of one long flat list. Addon asset types land in the matching category automatically.
Node menus got the same treatment. The Other bucket is now Input / Animation / Logic / Generic, and 3D and Widget each gained an Animation group. Transform animation nodes are cross-listed into the surface menus you'd actually look in.
Quick-spawn toolbars. Two rows of icon buttons in the Scene panel for the nodes you place constantly, and a row in the Assets panel for the assets you create constantly. Hover for the label.
Five shortcut buttons on the main menu bar. Preferences, App Settings, Player Input Panel, Addons Manager, Build Profiles , one click instead of a menu walk.
The Script Creator writes your callbacks for you. A Node Reactions dropdown covering fourteen node types , pick
ListViewWidgetand checkOnItemGenerate,OnSelectionChanged, and the rest, and the stubs come out with the right parameters. There's also a Lifecycle section for singleton and persist-across-scenes boilerplate, and property defaults now emitif self.X == nil thenso inspector-set values survive a reload.Rename, duplicate, and delete scripts from the Script Panel. Right-click any script. Rename rewrites the Lua class name inside the file to match, since those have to agree. Engine scripts stay read-only.
Importing a mesh brings its materials and textures. Point the importer at a GLB and get the mesh, its materials with alpha mode and cutoff mapped across from the glTF metadata, and its textures , including ones embedded in the GLB itself , optionally tucked into a subdirectory named after the file. The source material is assigned to the mesh automatically.
Replace With. Right-click a widget and morph it into a different type in place. It keeps its slot in the parent, its children, every property the new type also publishes, and , importantly , its persistent UUID, so Timeline bindings and node-graph references still resolve.
System.ListDirectory(path)returns an array of{ name, isDirectory }tables. Non-recursive, no error to handle , an unopenable path is just an empty list.Build All. One button, every profile, sequentially, stopping on the first failure. It survives closing the Packaging window mid-queue.
Copy Hierarchy actually copies everything now. Full inspector state , transform, size, anchor, color, opacity, pivot, texture, material , plus script-exposed fields, in the same JSON shape the controller REST API emits. Addon node types come along for free.
Game Preview knows your project's resolution. A
* Project Settingspreset sits at the top of the list, sourced from App Settings , the resolution your packaged game actually runs at. In Scene2D a cyan border marks the game-visible area, and widgets that overshoot it still render so you can grab them.Reload All Scripts sweeps disk. It used to only re-run scripts that were already loaded. Now it walks the engine, project, and addon script roots, so brand-new files and anything pulled in through
require()are covered without restarting.Real bounding boxes. Every primitive now reports a tight axis-aligned box , meshes, capsules, instanced meshes, voxels , and
GetHierarchyAABBunions a whole subtree. PressingFframes the box instead of a sphere. Two new debug modes draw them for all nodes or just the selection, and they're available from Lua and the addon API.Splines got an API. Length, closest distance along the curve, point access, add and clear, loop and smoothing toggles , from C++, Lua and addons.
Addon Browse tab has categories. The registry ships one auto-generated manifest per repository now instead of crawling package files one at a time, and the Browse tab groups addons under the categories the registry defines.
Import a scene with shared materials. Tick Share Materials/Textures and every scene from the same art pack reuses one
_Sharedfolder instead of stamping outM_Level3_Woodnext toM_Level4_Wood. glTF extras can name a material path, a collision mode and a hidden flag per node.Game Preview follows the active camera by default, so cutscene camera switches show up without touching the combo.
World:GetLoadedSceneName()and the plural, for code that needs to know where it is.Addon thumbnails go through a proper image cache, with deferred release so closing the Addons window mid-frame can't yank a texture out from under ImGui.
The Logging toggle works now. Flipping it in App Settings takes effect immediately, and a project that turned logging off in its
Config.iniis respected when you open it, not at the next launch.CSS
overflow: visibledoes something. Onlyhiddenwas handled, so a stylesheet could never turn scissoring off on a Button or a Canvas.visibleclears it,clipis accepted as a synonym.Texture:SetRetainPixels(true)keeps the CPU copy alive after the GPU upload in a shipped build, for a snapshot you're going to encode or save. Off by default because it costs width times height times four.Import an OBJ with no normals. The importer generated none and then read them. Assimp generates smooth normals now, and the reader tolerates a mesh without them.
Addon REST routes can answer with 256 KB, up from a 4 KB stack buffer. The GLB bundle downloader needed 600 requests for a 2 MB file before.
Duplicating a scene in Explorer gave the new tab the old name. The serialized name inside the file won over the filename. The editor reconciles them on load.
Import, combined import, retarget and open-scene are exported to addons, so a pipeline addon can drive a conversion without going through the interactive importer.
Bug Fixes Worth Calling Out
WAV files imported silent or as garbage. The importer read the data size at byte 40 and the samples at byte 44, assuming the
datachunk followedfmtdirectly. Every DAW that writes aLISTorINFOchunk broke it. It walks the chunks properly now, and stereo durations stopped reporting double.Nearest-filter textures looked linear on 3DS. Three causes stacked: the texture still generated averaged mip levels, the C3D backend used a linear mip filter, and the cook downsample smooth-resampled pixel art. All three respect Nearest now, and font atlases finally pick up filter changes.
Creating a project on Linux crashed the editor. The Game Preview released its ImGui descriptor set mid-frame while a queued image still referenced it. RADV segfaulted; Windows happened to tolerate it. Texture release is deferred to the next frame now.
Saving a scene with linked sub-scenes was pathologically slow. Override gathering instantiated the linked scene once per visited node, so a big prefab could trigger hundreds of full builds and a progress bar that looked stuck in a loop. One instance is shared per subtree.
A headless package shipped without engine textures. Engine asset import was skipped when headless, so PNG-backed engine textures had nothing to cook and the game crashed on its first UI quad.
A texture cook flipped every image written after it. The vertical-flip flag on the PNG writer is global and was never reset, so the 3DS icon, the CIA banner and the Android icon all came out upside down.
Editing the 3DS title or icon reported "up to date". Canonical targets had no option hash in the build-cache key.
The gamepad axis
+button was too narrow to click.Rigs with more than 127 bones corrupted bone attachments. The scene format stored the parent bone as a signed byte. Fingers and twist bones on a detailed rig went past it and wrapped. Widened, with the asset version bumped so old scenes still load.
Ctrl+R was re-firing during Reload All Scripts. The progress modal pumps editor frames while it works, but input didn't advance, so the hotkey stayed latched and re-ran the whole C# transpile between every script load.
Specular shader ignored light intensity.
ForwardSpec.fragused the raw color for directional and point lights whileForward.fragscaled by intensity, so the same scene lit differently depending on shading model.Every script reload leaked Lua stack slots. Results and error objects from
RunScriptwere never popped, so a long editing session slowly grew the main stack.3DS and Wii logged directory errors for folders that are allowed to be missing and called
closedir(nullptr)on the way out, which is undefined on newlib.Hot reload stopped working after reopening a project. The setting lives in the project's
Config.ini, but the live watcher flag was synced from it exactly once at engine startup , before any project is open. So the watcher sat there collecting events that got thrown away every frame, while the preferences checkbox cheerfully showed it as on. Toggling it off and on was the only thing that revived it. Launching with a-projectargument hid the bug entirely, which is why it looked intermittent.Projects under OneDrive silently shipped broken builds. Controlled Folder Access blocked the cook's writes, nothing surfaced an error, and you got a truncated package with empty embedded assets , which booted to a black screen on GameCube and Wii, where there's no filesystem to fall back on. Builds now probe for writability first and stop with a message that names OneDrive as the likely cause. New projects default to
%USERPROFILE%\PolyphaseProjectsinstead of Documents.Headless cooks segfaulted right after "Cooking assets...". Engine asset discovery was gated on the asset-registry flag, but the registry is an output of packaging, never an input to the cook. With the flag on, the engine asset directory was never discovered and the build dereferenced a null pointer, leaving a truncated log as the only clue.
Docker builds linked stale host object files. Bullet and Assimp intermediates were being copied into the container with their timestamps, so
makeinside the container trusted objects compiled by your host toolchain. If your host was newer than the image, the link failed on missing glibc symbols.Build All crashed on the second profile. There's exactly one frame where the build thread has exited but hasn't been joined yet, and the queue kicked the next profile straight into it , reassigning a live
std::thread, which terminates the process on the spot.Play in Fullscreen from a maximized editor gave you a window
width x 0. Windows reports the restored rect while the engine held the maximized client size, so the computed window chrome height went negative. Closing that window with the X button also used to shut down the entire app instead of just ending play mode.Asset picker dropdowns reopened the instant you picked something. The suppression latch compared against a static that every other picker on screen overwrote in the same frame, so it could never work as a per-picker snapshot. Multiple pickers also leaked keystrokes into each other.
ListViews didn't scroll and their rows didn't span. The scroll container looked for its content by scanning for the first non-transient child, and the list's internal container is transient , so it computed a content size of zero and never activated. Rows now fill the cross axis by default unless you've set a fixed size. List items could also be freed mid-callback by user Lua that rebuilt the list, and then used.
LineEdit margins changed depending on camera zoom. Authored pixel margins were being applied against a rendered-space rect, and the parent-size accessors returned scaled pixels in one viewport and authored pixels in another.
A corrupted property could take the editor down at save or shutdown. Several guards landed together: scene node-def teardown survives a faulted destructor by leaking rather than crashing, bogus property counts from a truncated
.octare clamped instead of fed toresize(), a cyclic widget hierarchy no longer overflows the stack every single frame, and aDatumstring with a garbage size field is rejected instead of terminating the process.Scaling a mesh to zero on any axis crashed collision traversal. Bullet reciprocates each scale axis when mapping a query box, so a flat axis produced infinite bounds, the quantized BVH cast those to garbage, and the triangle callback walked off into nothing. Near-zero axes are now padded, sign preserved, so negative-scale mirroring still works.
Addons could fail to load with Windows error 126 right after building. Companion DLLs sat in the build directory but not in the shadow-copy directory the loader actually searches. The shadow copy now brings the whole directory along.
A stale asset pointer surviving a project switch crashed on the first frame. Quads now verify the asset is live before dereferencing what could be a dead vtable.
ImGui asserted the first time you docked an addon window when the layout root already contained a split, because the dock target resolved to an unnamed container.
Changing a texture's filter from Linear to Nearest froze the whole machine. The texture rebuilt its GPU image behind the same pointer, and every ImGui preview that had cached a descriptor for it kept pointing at freed memory. The GPU faulted, the profiler's timestamp query saw a device-lost error, and an assert took the editor down with the display driver still wedged. Every cache now keys on a resource generation counter, and the profiler disables itself instead of asserting.
Clicking a tab during a long operation crashed the editor. The progress bar pumps full editor frames while it works, so a click could open or close a scene mid-instantiate and leave the outer operation holding freed pointers. Scene open and close are now blocked while the progress pump is active.
Arrays vanished from the Properties panel. A deep copy dropped the vector flag on internal-storage properties. One-line fix, long-standing symptom.
A spline grew a phantom point every time you reloaded the scene, and its clamp broke the 3DS build outright. Both fixed.
Deleting an asset folder left the browser holding a dangling pointer, and a failed directory move updated the in-memory tree anyway.
Spawning an unknown node type over the REST controller crashed the editor instead of returning an error.
Mounting a pak from memory crashed. The debug log line read the mount's name after the mount had been moved into the stack. A null deref disguised as string corruption.
Saving into a new nested asset folder did nothing.
SaveAssetmade one directory level and failed silently on the second. Missing ancestors are created root to leaf.A skeletal mesh loaded from disk reported a unit bounding box. The sphere was restored, the AABB wasn't.
Createrecomputes it, which the character creator needed to put feet on the floor.Sha256::HashHexgave addonsLNK2019. MSVC dropped the object because nothing in the exe referenced it,POLYPHASE_APIor not. It's force-linked now, which then broke the 3DS, GameCube and Wii links because console Makefiles list source directories by hand.Source/Engine/Utilsis in all three.The Windows SDK release script crashed on
osbeing undefined. One missing import, every release run since the Vulkan header staging landed.A GCC-built editor wrote a corrupt Mac
PkgInfo.'APPL????'contains a trigraph and GCC rewrote it. Escaped.The Vulkan portability flag broke the Android build. The NDK's headers don't declare it. It's Mac-only now, which is the only place it was ever read.
A Note on the Licence
The repository licence is now MIT with a Proof-of-Usage condition (MIT-PoU). Every MIT permission is unchanged and nothing about building, shipping or selling a game with Polyphase has changed. The one addition asks operators of automated systems , training pipelines, RAG indexes, code assistants , to record that they used the repository and to credit it. Disclosure, not restraint. If you're a human reading, forking, learning from or shipping with the engine, this does not apply to you. Details are in LICENSE, README.md and PROOF_OF_USAGE.md.
Built for the people who ship.