Changelog - v6.2.0-beta18
Range: dcea3206..0c8160d4
Feature
macOS Platform
Platform::Macappended afterPsp(value 7, serialized values preserved),PlatformBit_Mac,StringToPlatformgains"PSP"and"Mac",GetPlatform()returns Mac underPLATFORM_MAC.ASSET_VERSION_INPUT_PROMPT_MAP_PLATFORM_MAC = 43,ASSET_VERSION_CURRENT = 43; pre-v43InputPromptMapfiles remap raw7back toPlatform::Countso "Any" entries do not become Mac. Legacy sidecar masks naming all six original platforms (kLegacyNamedAllMask) now meanPlatformBit_Allso PSP and Mac are not excluded.- Build.
PLATFORM_MAC=1+API_VULKAN=1over MoltenVK. New Makefiles mirroring the Linux ones line for line:Engine/Makefile_Mac,Standalone/Makefile_Mac_Editor,Standalone/Makefile_Mac_Game,Template/Makefile_Mac_Editor,Template/Makefile_Mac_Game,External/Bullet/Makefile_Mac,External/Assimp/Makefile_Mac. Objects inIntermediate/Mac/, outputs inBuild/Mac/; editor atStandalone/Build/Mac/PolyphaseEditor, runtime atStandalone/Build/Mac/Polyphase.macho(the suffix avoids colliding withPackaged/Mac/<Project>/).MACHDEP := -arch arm64 -mmacosx-version-min=12.0;-std=gnu++17;.mmfiles compile-ObjC++ -fobjc-arc; link-rdynamic -Wl,-rpath,@loader_path -Wl,-rpath,@loader_path/../Frameworks -Wl,-rpath,$(VULKAN_SDK)/libwith Cocoa, Metal, QuartzCore, IOKit, GameController, AudioToolbox, CoreAudio, Security, CoreFoundation and Carbon; the editor adds-lAssimp -lBullet -lgit2 -liconv -framework GSS -lshaderc_combined -lspirv-cross-core. Shared variants (POLYPHASE_SHARED=1/POLYPHASE_EDITOR_SHARED=1) emitlibPolyphaseGame.dylib/libPolyphaseEditor.dylibfrom PIC objects inIntermediate/Mac-pic. Applemakeis 3.81, so the Mach-O link rule is explicit; Assimp filtersOFILESto%.obecause Appleldrejects raw source names.Makefile_Mac_Gamehonours-include $(PROJECT_ROOT)/Generated/AddonInject.mkand recurses via$(firstword $(MAKEFILE_LIST))so the packager'sMakefile_TEMPsplice is respected.CMakeLists.txtgainselseif(APPLE), but the Makefiles are canonical.Tools/prebuild_mac.shverifies$VULKAN_SDK/bin/glslc,cmakeandxcode-select -p, then runsprebuild_libgit2.sh(Darwin adds-DUSE_HTTPS=SecureTransport -DCMAKE_OSX_ARCHITECTURES=arm64 -DCMAKE_OSX_DEPLOYMENT_TARGET=12.0), the shader compile, and the embedded-stub generator.External/Zlib/zutil.h'sOS_CODE 7block now excludes__APPLE__.<malloc.h>is guarded#if !PLATFORM_MACinTexture.cpp,ContentObfuscation.cpp,ContentPak.cpp,Renderer.cpp,Stream.cpp. - Toolchain. Xcode Command Line Tools (
clang,ar,ld64), Homebrewcmake python3, LunarG Vulkan SDK for macOS 1.4.357.1 (MoltenVK, loader,glslc,libshaderc_combined.a,libspirv-cross-core.a) withVULKAN_SDKpointing at~/VulkanSDK/<ver>/macOS.wss://needs Homebrew curl via the newPOLYPHASE_LIBCURLenv override (honoured on Linux too). System_MacCocoa.mm.NSApplicationbootstrap with menu bar,PolyphaseMetalView(flipped,CAMetalLayer, Finder drag-drop →SYS_DrainDroppedFiles),PolyphaseAppDelegate(Cmd-Q and close setmQuit), a pixel coordinate model in backing pixels (drawableSize, mouse ×backingScaleFactor,SystemState::mBackingScale,SYS_GetDisplayScale()), a polledNSEventpump feedingINP_*andImGui_ImplMac_*, left/right modifier bits with Command aliased ontoPOLYPHASE_KEY_CONTROL_L/R, trackpad precise scroll × 0.02 accumulated to ticks,CGWarpMouseCursorPosition, Alt+Enter fullscreen,NSOpenPanel/NSSavePanel,NSPasteboard,openfor reveal.SYS_OPEN_CMDmacro ("open"vs"xdg-open").GameMainchdirs to bundle Resources whenConfig.iniis not in cwd.System_Mac.cpp(POSIX half).SYS_GetBundleResourcePath(), bundle-awareSYS_GetPolyphasePath,posix_memalignwith alignment clamped tosizeof(void*), RAM viatask_info(MACH_TASK_BASIC_INFO), CPU% from task basic + thread times,SYS_GetTotalRAMviahw.memsize,SYS_GetPlatformTier() = 2, double-forkSYS_SpawnDetachedExecutable,SYS_CopyFileverified bystat. Save directory:$POLYPHASE_SAVE_DIR→<projectDir>/Savesif writable →~/Library/Application Support/<Project>/Saves.SYS_CloseDirectorynull-guards becauseclosedir(NULL)segfaults on macOS.Audio_Mac.cpp.DefaultOutputAudioUnit fed by a lock-free SPSCSampleRing;kMixSampleRate 44100,kMixChannels 2,AUDIO_MAX_VOICES 8, playback buffer 1/15 s, ring two update periods. Streaming poolkMaxStreamingVoices = 4, one AudioUnit per voice, 16-bit mono/stereo only, 1 s ring with soft backpressure (AUD_SubmitStreamBufferreturns 0),mFramesPlayedcounts real frames only so the clock stalls rather than drifts,AUD_FlushStreamvia atomicmReset.Input_Mac.mm. GameController.framework polled per frame intoINPUT_MAX_GAMEPADSslots; DualShock 4 and DualSense viaproductCategory; triggers →L2/R2at > 0.5; balancedNSCursor hide/unhide; cursor trap viaCGAssociateMouseAndMouseCursorPosition; CarbonkVK_*KeyCodeenum; presets under~/Library/Application Support/PolyphaseEditor/InputPresets.Network_Mac.cpp,Serial_Mac.cpp(termios raw mode; enumerates/dev/cu.usb*,cu.usbserial,cu.usbmodem,cu.SLAB,cu.wchusb), andHttpBackend_Linux.cpp/WebSocketWss_Linux.cppnow build on Mac withlibcurl.4.dylibdlopen candidates plus Homebrew paths.- Vulkan / MoltenVK.
VK_EXT_metal_surface,VK_KHR_portability_enumeration+VK_INSTANCE_CREATE_ENUMERATE_PORTABILITY_BIT_KHR,vkCreateMetalSurfaceEXTonmMetalLayer,VK_KHR_portability_subsetwhen advertised. Metal has no triangle fans, soQuad::kExpandFanToList(kMaxFanVertices 38, MackMaxQuadVertices = 108) andVK_PRIMITIVE_TOPOLOGY_TRIANGLE_LISTon Mac.VulkanContext::CreateInstancenow warns and continues without validation layers instead of asserting (shipped.app, no SDK on the layer path).MaterialBase::Compileresolves$VULKAN_SDK/bin/glslcor PATHglslcon POSIX.Engine/Shaders/GLSL/compile.shrewritten: the oldval= $VULKAN_SDK/bin/glslc …never captured a status and[ val != 0 ]was always true. - Editor.
imgui_impl_mac.{h,mm}, a Cocoa backend forked fromimgui_impl_osx.mmand restructured likeimgui_impl_xcb(engine pump feedsImGui_ImplMac_EventHandler, backing pixels, IME viaNSTextInputClient).EditorMain::SetupMacToolchainEnvironment()fillsVULKAN_SDK,VK_DRIVER_FILES,VK_ADD_LAYER_PATH,DEVKITPRO/PPC/ARMand PATH for Finder launches (readsExternal.jsonvulkanSdkPath); Retina defaultsmEditorInterfaceScaleto the display scale unlessEditorInterfaceScaleis explicit inConfig.ini(EngineConfig::mEditorInterfaceScaleExplicit).NativeAddonManagerbuilds addons withclang -dynamiclib -arch arm64 -mmacosx-version-min=12.0 -Wl,-undefined,dynamic_lookupaslib<binaryName>.dylib;ModuleLoaderloads.dylib; terminal usesforkptyfrom<util.h>;AutoUpdaterprefers.dmg; preferences under~/Library/Application Support/PolyphaseEditor; Docker disabled for Mac profiles;GamePreviewdefaults to 1920x1080;BuildDependencyWindowadds Mac-onlyCheckXcodeTools()andCheckGlslc();.NEThintbrew install --cask dotnet-sdk. VS Code:Polyphase Editor - Mac/Polyphase Game - Mac(lldb) with matching make tasks. - Packaging games —
MacBundlePackager(polyphase.mac, "macOS (App Bundle)"), the canonical target forPlatform::Mac. Compile reusesMakefile_Mac_Game(make -C {BuildProjDir} -f Makefile_TEMP -j 12,strip -S);PostPackagewrapsPackaged/Mac/<Project>.macho, the loose payload,libvulkan.1.dylib+libMoltenVK.dyliband a bundle-relativeContents/Resources/vulkan/icd.d/MoltenVK_icd.jsoninto<Project>.app; addons go toContents/MacOS/Addons/;copyBinariesstage intoContents/Frameworks. Options:mac.bundleId(com.polyphase.<AppName>),mac.version(1.0.0),mac.minOsVersion(12.0),mac.iconPath(.icnsviasips+iconutil),mac.signingIdentity,mac.notarize,mac.notaryProfile,mac.createDmg(hdiutil); any change alters the build-cache variant key. Bundles are always at least ad-hoc signed (codesign --sign -) because Apple Silicon refuses unsigned arm64 andinstall_name_toolinvalidates signatures; distribution signing uses--options runtime --timestampwithdisable-library-validation,allow-unsigned-executable-memoryandallow-dyld-environment-variablesentitlements, thenxcrun notarytoolandstapler. Build & Run usesopen -n; headless-build Mac embedded.verify_project_build.sh … Maclocates the.appand runscodesign --verify --deep --strict. NewActionManager::ResolveCompiledBinaryDir()andRunPostPackage(), which now also runs on the cache-hit path because banner, RSF and tool inputs are not in the manifest. - Shipping the editor.
Installers/build_app_mac.shrunsstage_distribution.py --platform macand buildsdist/Polyphase.app(CFBundleIdentifier com.polyphase.editor,LSMinimumSystemVersion 12.0, exports UTIcom.polyphase.projectfor.octp, bundle-relative ICD withis_portability_driver), signing dylibs,Polyphase.machoand the bundle inside-out; envMAC_SIGN_IDENTITY,MAC_NOTARY_PROFILE,SKIP_STAGE=1.Installers/build_dmg_mac.shproducesdist/PolyphaseEditor-<version>-macos-arm64.dmg(UDZO,/Applicationssymlink, threehdiutilretries).stage_distribution.pygains themacplatform andstage_vulkan_runtime_mac()(viaresolve_vulkan_sdk_mac()). Automated in beta.18, see CI below. - *CI (beta.18).* New composite action
.github/actions/install-vulkan-sdk-mac(inputsversiondefault1.4.357.1,polyphase-repodefaultPolyphase-Labs/Polyphase-Engine,mirror-urlfull-URL override; outputvulkan-sdk-dir). It downloadsvulkansdk-macos-<ver>.zipfrom avulkan-sdk-<ver>GitHub Release on the engine repo first and LunarG's CDN second (with a::warning::), because LunarG cycles old SDKs off the CDN and old releases should stay reproducible; skips the download whenbin/glslcandlib/libMoltenVK.dylibalready exist; drives the InstallBuilder.appheadlessly (--accept-licenses --default-answer --confirm-command install); hard-fails unlessbin/glslc,lib/libMoltenVK.dylib,lib/libvulkan.1.dylib,lib/libshaderc_combined.a,lib/libspirv-cross-core.aandshare/vulkan/icd.d/MoltenVK_icd.jsonexist; exportsVULKAN_SDK=$HOME/VulkanSDK/<ver>/macOSand prependsbintoPATH. Newrelease.ymljobbuild-macon pinnedmacos-15(Apple Silicon; the-largerunners are Intel and the Makefiles hardcode-arch arm64),needs: publish-docker-images: submodule init,actions/cache@v4on~/VulkanSDK/1.4.357.1(keyvulkan-sdk-macos-1.4.357.1), the action,bash Tools/prebuild_mac.sh,make -C Standalone -f Makefile_Mac_Editor -j$(sysctl -n hw.ncpu),make -C Standalone -f Makefile_Mac_Game(staged into the bundle atContents/Resources/Standalone/Build/Mac/Polyphase.machoso the Packaging window can ship script-only projects),make -C External/Lua a SYSCFLAGS="-DLUA_USE_MACOSX" MYCFLAGS="-arch arm64 -mmacosx-version-min=12.0"copied toExternal/Lua/Build/Mac/arm64/ReleaseEditor/libLua.a(theatarget, notmacosx, which would also linklua/luacagainst readline) with its own verify step becausestage_distribution.pyonly warns when it is missing,bash Installers/build_dmg_mac.sh, a glob that errors unless exactly onedist/PolyphaseEditor-*-macos-arm64.dmgexists,hdiutil verify, upload as artifactmacos-dmg.create-releasenowneedsbuild-mac, downloads{windows-installer,windows-sdk,linux-deb,linux-tarball,macos-dmg}and attachesPolyphaseEditor-*-macos-arm64.dmg. Ad-hoc signed only:MAC_SIGN_IDENTITY/MAC_NOTARY_PROFILEare documented in the job but not wired, so first launch is right-click → Open. The Linux-only smoke addon and theverify-test-projectmatrix are not ported.Mac.mdnotes the SDK version now lives in three places (doc, action default, cache key). - *Input fixes (beta.18),
imgui_impl_mac.mm/System_MacCocoa.mm.* (1) Cmd+A/C/V/X/Z were dead in every text field:ImGui_ImplMac_UpdateKeyModifiersfed ImGui from the engine key array, where Command is aliased onto Control for the editor's Ctrl-named hotkeys, but underConfigMacOSXBehaviorsImGui resolvesImGuiMod_Shortcutto Super andShortcut()matches the modifier set exactly, so a Cmd press that also read as Ctrl never matched. Modifiers now come straight from[NSEvent modifierFlags](Control/Shift/Option→ImGuiMod_Ctrl/Shift/Alt,Command→bd->SuperDown), which also self-heals a modifier left stuck by a Cmd+Tab whose key-up went to another app. (2) Held keys typed one character, and typing could be dead entirely:ImGui_ImplMac_EventHandlerreturned on[event isARepeat]before the character path, and when the responder chain was brokeninsertText:never fired. The repeat guard now only wrapsAddKeyEvent/SetKeyEventNativeData; newImGui_ImplMac_Data::CharsThisEventcounts characters the responder produced, and when it produced none for aNSEventTypeKeyDownthe handler walks[event characters]intoio.AddInputCharacter, skipping control codes (< 32,127) and the0xF700–0xF8FFprivate-use block AppKit uses for arrows, F-keys, Home and End.SYS_Updatenow runs the ImGui handler after[NSApp sendEvent:](engine state → Cocoa dispatch → ImGui) so it can tell whether the responder produced characters. (3) Keyboard lost after focus changes, fullscreen or a native file dialog: newIMGUI_IMPL_API void ImGui_ImplMac_EnsureKeyResponder()re-asserts theNSTextInputClientresponder as first responder, called fromImGui_ImplMac_Init,windowDidBecomeKey:,windowDidEnterFullScreen:,windowDidExitFullScreen:, and viaRestoreKeyResponderAfterModal()at the end ofSYS_OpenFileDialog,SYS_SaveFileDialogandSYS_SelectFolderDialog. EditorShortcutModDown()(beta.18) — new inline inEditorImgui.h(ImGui::IsKeyDown(ImGuiMod_Shortcut): Cmd on macOS, Ctrl elsewhere) replacesio.KeyCtrlwherever it meant "shortcut or multi-select modifier" rather than the physical Control key a terminal still needs: Ctrl-click toggle-select in the Scene and Scripts panels,EditorHotkeyMap::IsActionJustTriggeredImGui(every ImGui-routed hotkey binding now fires from Cmd), Debug Log (entry toggle, Ctrl+C, Ctrl+A), CLI Terminal (range and toggle select, Ctrl+A, pipe-mode Ctrl+C), Packaging Build Output (same), Node Graph (Ctrl+C/V/D), Script Editor (Ctrl+S/O/N). Behaviour-preserving on Windows and Linux.- Consoles from a Mac host. devkitPro's pacman
.pkg,sudo dkp-pacman -S wii-dev 3ds-dev, the libogc2 key import (with an HTTPScurlfallback when HKP port 11371 is blocked), a[libogc2-devkitpro]entry withmacos/$archmirrors, andgamecube-tools-git libogc2 libogc2-libdvm. No WSL analogue is needed. - Limitations. Apple Silicon only (Intel and universal binaries unsupported;
MACHDEPis where they would go); the compute path tracer for light baking stays Windows/Linux;wideLinesis unavailable on Metal so debug lines are 1 px; no Docker builds; the Lua debugger transport is Windows-only; Finder double-click of an.octparrives as an Apple Event (useopen -a Polyphase --args …).
Occlusion Culling
OcclusionData(newEngine/OcclusionData.h/.cpp) — a per-scene potentially-visible set over a uniform grid (mMin,mCellSizedefault4.0f,mDimX/Y/Z). Each cell maps to a bitset of occludees (mWordsPerSetwords); identical bitsets are deduplicated throughmSetPool; cell → set indices are 16-bit (mCellToSet16,0xFFFF= no set) whenever every index fits, else 32-bit. API:IsValid,Clear,FindCell(vec3),GetCellSetIndex,GetSet,IsVisible(set, occludeeIndex)(null set ⇒ visible),GetCellAABB,GetVolumeAABB,GetNumCells,GetNumUniqueSets,GetStaticTableBytes / GetDynamicTableBytes / GetMemoryBytes,SaveStream,LoadStream(stream, version).mBakeAlgorithmrecords the sampling scheme so the baker only reuses bits from a bake with the same scheme.- Dynamic coarse table. Optional second table of
mCoarseFactor-cubed blocks (mCellToCoarseSet16/32,mCoarseSetPool).IsBoxVisibleDynamic(cell, AABB): a moving occludee is visible if any coarse cell its box overlaps is visible; boxes partly outside the volume or covering more thankMaxDynamicCoarseCells = 64coarse cells are reported visible. - Versions.
ASSET_VERSION_SCENE_OCCLUSION = 40(cell table as plain uint32),ASSET_VERSION_SCENE_OCCLUSION_DYNAMIC = 41(width byte, coarse table,mOcclusionDynamic,mOcclusionConsoleBudgetKB),ASSET_VERSION_SCENE_OCCLUSION_ALGO = 42(mBakeAlgorithm, defaults to1for v41 data). OcclusionEntryIndex— tolerant lookup of bakedOcclusionEntry { mCenter, mExtents }records, bucketed byfloor(center / max(cellSize, 1)), probing the 27 neighbouring buckets and accepting the closest entry withinmax(0.1 * cellSize, 0.01 * maxExtent). Play-in-editor clones and re-instantiated trees rebuild rotations from euler properties, so float noise on a large rotated wall exceeds any exact quantisation; identity by UUID was rejected for the same reason.
- Dynamic coarse table. Optional second table of
- Scene properties (category
Occlusion Culling):Occlusion Culling(false),Occlusion Cell Size(4.0),Occlusion Bake Quality(Low / Medium / High, default Medium),Occlusion Dynamic Objects(true),Occlusion Console Budget KB(512,0= unlimited). Console budget strip:Scene::SaveStreamfor a console platform writes an emptyOcclusionDatawhenGetMemoryBytes()exceeds the budget and logs a warning; the editor copy always keeps it. Primitive3DcategoryOcclusion:Occluder Static/Occludee Static(bothfalse),EnableOccluder / IsOccluder / EnableOccludee / IsOccludee, runtimeSetOcclusionSlot(slot, bakedCenter)/GetOcclusionSlot()(0 = none) /GetOcclusionBakedCenter(), andSetOcclusionMatchBox(localAABB)for nodes whose match box differs from mesh bounds.OcclusionArea3D(new node,Extentsdefault{20, 10, 20}, cyan wire-cube gizmo, Add Node → Environment) bounds the bake volume; without one the volume is the union of occludee bounds.- Runtime.
DrawData::mOcclusionSlotwithkDynamicOccludee = 0xFFFFFFFF;Mesh3D::GetDrawDatauses the static slot only whiledistance2(center, bakedCenter) <= (0.25 * cellSize)², else the dynamic path.World::SetOcclusionData / IsOcclusionCullingEnabled / IsOcclusionDataStale / RequestOccludeeResolve / ResolveOccludees; resolve is deferred toRenderer::GatherDrawData, callsGetTransform()beforeGetOcclusionMatchAABB()because freshly cloned trees have dirty transforms, and flags the data stale (with an editor toast) if any baked occluder is unmatched. Eligible occludee types:StaticMesh3D,SkeletalMesh3D,Terrain3D,Voxel3D,TextMesh3D; neverSkybox3DorShadowMesh3D. Children of an unrolledInstancedMesh3Dinherit the parent's slot;InstancedMesh3D::ComputeUnrollBucketsis shared with the baker so cells match on every platform.Renderer::OcclusionCullruns beforeFrustumCull(a bit test per draw, so frustum math is only spent on survivors), skips orthographic cameras, leavesmShadowDrawsalone so hidden casters still shadow, never culls depthless draws, and routes culled draws throughHandleCullResult(false)so skeletal and particle "always update" modes apply. The editor camera never drops draws; with View > Occlusion Preview on, it draws the volume, the game camera's cell, and per-occludee green/red boxes.Renderer::EnableOcclusionCulling / GetNumFrustumCulled / GetNumOcclusionCulled;StatDisplayMode::Cullingon the stats overlay.Maths::RayIntersectsAABB(slab) andMaths::RayIntersectsTriangle(Möller–Trumbore, double-sided) added for the baker. - Baker —
OcclusionBaker(Editor/OcclusionBake/),BakeCurrentScene()/ClearCurrentScene(), run from the end-of-frame dispatcher, cancellable, multi-threaded (hardware_concurrency() - 1, capped at cell count), statusCell N / M (K occludees[, incremental], T triangles[, + dynamic table]). Occluder triangles come from opaque-materialStaticMesh3D,Terrain3DandVoxel3D;InstancedMesh3Doccludees also bake one extra occludee per unroll cell so console-unrolled children cull individually. Median-splitTriangleBvh(kMaxTrisPerLeaf = 4); LCG cell samples inset 2% from cell faces; samples inside solid geometry (≥4 of 6 axis probes hit back-faces within4 * cellSize) are discarded and a fully solid cell is marked all-visible. Targets are box centre + 8 corners + optional face centres + a face grid + up to N mesh vertices; quality presets 0 → 4 samples / 0 verts, 1 → 9 / 8 / face centres, 2 → 17 / 24 / face centres.kWarnCells = 65536,kMaxCells = 262144(hard error suggesting a larger cell size orOcclusionArea3D),kLargeOccludeeCells = 8,kTargetCoarseCells = 512(coarseFactor = clamp(ceil(cbrt(numCells / 512)), 4, 16)), cell size floored at 0.25. Incremental: if dims, cell size, volume, occluder count andmBakeAlgorithmmatch and every occluder matches viaOcclusionEntryIndex, pre-existing occludees' bits (and the dynamic table whenmCoarseFactormatches) are copied rather than re-traced. Output bitsets are deduplicated (PackSets, FNV hash) and a warning fires when static + dynamic bytes exceed the console budget. - Editor wiring. World menu:
Bake Occlusion Culling,Clear Occlusion Data(disabled in play-in-editor),Mark Selected Occluder + Occludee. View menu:Occlusion Preview. A top-centre toast withRe-bake/Dismissappears when resolve flags stale data. REST:POST /api/occlusion/bake,POST /api/occlusion/clear,GET /api/occlusion/status(enabled, stale, baking, cells, unique sets, bytes, culled counts, camera cell, per-node slot and moved state),PUT /api/occlusion/preview. - Lua.
Primitive3D:EnableOccluder(bool) / IsOccluder() / EnableOccludee(bool) / IsOccludee();Renderer.EnableOcclusionCulling(bool) / IsOcclusionCullingEnabled() / GetNumFrustumCulled() / GetNumOcclusionCulled(). - SeenByCamera.
World::IsSeenByCamera(Node3D*, Camera3D* = nullptr)andWorld::GetSeenByCamera(std::vector<Node3D*>&, const std::vector<std::string>* tags = nullptr, Camera3D* = nullptr)— a frustum sphere test on the node's bounds plus the static bit or dynamic table; non-primitives use a 0.25 radius sphere;ComputeMatrices()is called on demand so results never lag a frame; hidden nodes and the camera itself are never seen. Lua:World:IsSeenByCamera(node, camera?) → boolean,World:GetSeenByCamera(tags?, camera?) → Node[](tags as a string or array). Note: the generatedWorld.luastub declaresGetSeenByCamera(camera) → Node, which does not match the binding.
3DS CIA Export
- New built-in target
polyphase.n3ds.cia("Nintendo 3DS (CIA)",Editor/Packaging/CiaPackager.{h,cpp}) sharesPlatform::N3DScook and compile with the canonical.3dsxtarget;PostPackagestages romfs (stripping.3dsx / .cia / .smdh / .elfso stale CIAs do not ride along), regenerates the SMDH withsmdhtool, substitutes$(APP_TITLE),$(APP_PRODUCT_CODE),$(APP_UNIQUE_ID)and$(APP_ROMFS)intoStandalone/3DS/template.rsf(orcia.rsfPath) at<Project>/Intermediate/3DS_CIA/app.rsf, and runsmakerom -f cia … -target t -exefslogo(test keys). Output inPackaged/polyphase.n3ds.cia/. A CIA gets the full application region: 64 MB on O3DS, 124 MB on N3DS (SystemMode 64MB/SystemModeExt 124MBin the RSF);IORegisterMapping 1ff00000-1ff7ffffis required orndspInitdata-aborts. Install via FBI or Azahar; the HOME Menu caches banners by Title ID, so delete the title before reinstalling. - Options.
cia.productCode(defaultCTR-P-+ 4 chars),cia.uniqueId(default0xFF000 | FNV1a(project) & 0xFFF, the homebrew block; Title ID0x000400000<id>00),cia.version(6.6.4-packed, max 63.63.15),cia.rsfPath,cia.bannerMode(image/scene),cia.bannerImage(256x128),cia.bannerScene,cia.bannerRotate,cia.bannerSpin(30),cia.bannerRotMin / Max(0 / 360),cia.bannerAudio,cia.bannerAudioLoop,cia.bannerAudioAdpcm. Shared SMDH optionsn3ds.title / description / author / iconPathapply to both 3DS targets under a3DS Target Optionsheader and reach make asAPP_TITLE,APP_DESCRIPTION,APP_AUTHOR,ICON(icon resized to 48x48 from any stb-readable image or Texture asset). - Banner. Audio forced to 16-bit stereo 44.1 kHz (
kBannerAudioRate = 44100), capped atkBannerAudioMaxFrames = 96000(about 2 s) with a 50 ms fade — 32 kHz CWAV plays as beeps on hardware. PCM16 viabannertool makecwavby default; DSP-ADPCM viacwavtoolopt-in. 3D banners:BannerGltfExporterwritesbanner.gltf / .bin / tex_*.png(power-of-two textures ≤ 256) with nodesBannerOrigin → BannerSpin → BannerFitfitted tokTargetRadius = 10atkLookAtY = 1; spin is about local Z under a −90° X tilt because pycgfx decomposes Y viaasin; keys land on whole 60 fps frames (kMinFramesPerKey = 15,kMinCycleFrames = 60,kMaxSegments = 16) because fractional-frame keys froze the HOME Menu. Lighting rides glTFextras.polyphase.{lightDirection, lightColor, ambient}intoStandalone/3DS/banner_cgfx.py, which calls pycgfx'sconvert_gltf/writeand then setsTheLightand every material's ambient. A CGFX over 512 KB falls back to the image banner. NewScene::GetAmbientLightColor()accessor. - Tools. None ship with devkitPro.
CiaPackager::ResolveToollooks in Preferences > External > Launchers >3DS CIA Tools(makeromPath,bannertoolPath,cwavtoolPath,pythonPath,pycgfxPath), then the tools folder (%APPDATA%\PolyphaseEditor\Tools\3DS,~/.config/PolyphaseEditor/Tools/3DS,~/Library/Application Support/PolyphaseEditor/Tools/3DS), then<devkitPro>/tools/bin, then PATH. A Download button fetches makerom v0.19.0, bannertool 1.2.3 (no macOS release; build from source) and cwavtool 1.0.0; Install pycgfx pins a commit and runspip install --user gltflib pillow(pycgfx has no licence and is never bundled). The Build Dependencies window lists makerom as optional. - Headless.
-build <arg>now accepts a dotted target id (EngineConfig::mBuildTargetId) as well as a platform; headless mode creates theEditorUIHookManager, loads the matching build profile (target options, static content, Content Pak) and reachesPostPackage.PolyphaseEditor -headless -project MyGame/MyGame.octp -build polyphase.n3ds.cia embedded.
Content Pak Mount Stack and PS2 Hardware I/O
- *Mount stack (beta.17).*
ContentPakwent from one module-level mounted archive (sPakFile/sRecords/sPathBlob) tostd::vector<std::unique_ptr<PakMount>> sMounts.Mount()pushes a new top-priority layer;Find()searches newest-first so a later pak shadows an earlier one instead of evicting it (the stated use: a downloaded bundle layered over the baseContent.pak). API, allPOLYPHASE_API:typedef uint32_t MountHandle(0 never valid);MountHandle Mount(const char* pakPath)(wasbool);MountHandle MountMemory(const void* data, uint32_t size, const char* debugName, bool takeOwnership)— a pak straight out of RAM for platforms with no writable storage for a downloaded bundle (the header names GameCube memory cards);takeOwnershipmeansfree()on unmount, so the buffer must bemalloc-allocated, whichStreamand HTTP bodies are, and ownership is handed back on a failed mount;void Unmount(MountHandle)(was no-arg);void UnmountAll();IsMounted()now means "at least one mount";FindEntry(path, MountHandle& outMount, uint32_t& outDataOffset, uint32_t& outDataSize)(signature changed);GetPakPath(MountHandle);GetMountMemory(MountHandle, const uint8_t*&, uint32_t&);List(prefix, …)merges across mounts newest-first with de-duplication so a shadowing entry wins (this is how the Vulkan global-shader enumeration fromEngine/Shaders/GLSL/bin/works in a packed build, where there is no directory to walk). Header parsing is factored intoParseAndCheckHeader()/ParsedHeader/FinishMount()shared by file and memory mounts; memory mounts bounds-checkindexOffset + indexSize <= sizeand copy the index out beforeDecodeInPlacemutates it. Per-mountmMutexreplaces the globalsPakMutex; memory mounts skip the lock.PakMountsits behindunique_ptrso apush_backcannot invalidate a handle aSysFilestill holds. Index format, FNV-1a hashing, binary search and collision-degrades-to-miss are unchanged. Source-breaking for anything calling the oldMount/Unmount/FindEntry/GetPakPath— see Deprecated. SysFile(SystemUtils.cpp) gainedmMemoryData/mMemorySize.SYS_FileOpenReadcalls the four-argFindEntrythenGetMountMemory; a memory-mounted entry reads straight from the buffer with nofopen, a file-backed entry opens an independentFILE*onGetPakPath(handle)so seekable streaming does not fightContentPak::Readfor file position.SYS_FileRead/SYS_FileSeekhave memory-backed branches (the offset is recomputed frommPayloadBase + mDecodedPoseach call, since there is no OS cursor);ContentObfuscationdetection and decoding work on both paths. Caveat: the memory path bounds-checks against the whole pak buffer rather than the entry, so an over-read at an entry boundary returns adjacent pak bytes instead of a short read.- Registry fold-in for addons.
AssetManager::DiscoverAssetRegistry()andRegisterAsset()are nowPOLYPHASE_API, as are theAssetDir,Sha256andCanvasclasses. An addon can callDiscoverAssetRegistryonce per downloaded bundle to add its stubs to the live registry (additive, never clears the lookup maps, works withEDITOR=0) andRegisterAsseta stub whosemPathis a key inside a mounted pak, resolved later throughStream::ReadFile. Pass a non-nullAssetDir*ormPathis empty and the asset never persists. - *PS2 hardware (beta.18),
ContentPak.cpp/SystemUtils.cpp.* Established on hardware; PCSX2 reproduces none of it. Everyfopen/fseek/freadon PS2 is a SIF RPC through one shared, unguarded client buffer that the audio mixer thread also uses every ~2 ms; cdvdman (OPL) serves an unaligned or partial-sector request as three reads; and a plainfreadof the header landed in a recycled newlibFILEbuffer that came back as garbage over SMB. Symptom:not a valid pak, a scrambled index, failed asset loads. NewReadThroughAlignedBounce(FILE*, pakPath, offset, size, dst): a static 32 KB aligned bounce (kBounce = 32*1024,kSector = 2048,kAlign = 64) that rounds every request to whole sectors, slices reads so the mixer is never starved for more than one slice, takes the SIF lock per request and copies out the sub-range; header, index andContentPak::Readall go through it. New hook macros, no-ops off PS2, implemented in the PS2 target'sSystem_PS2.cpp:PAK_DIRECT_READ→Ps2_DiscReadRange(path, off, n, dst, got)(disc boots read withsceCdRead;falsefalls back tofread),PAK_IO_LOCK/PAK_IO_UNLOCK→Ps2_SifLock/Ps2_SifUnlock,PAK_IO_SYNC→Ps2_SyncDCacheRange;Mount()'s twofopencalls are bracketed by the lock. Handing the IOP an uncached0x2000_0000alias was tried and rejected (DMA never lands there; ps2sdk's own write-back/invalidate is enough). The index read is retried up to 3× —DecodeInPlacechecksums the whole index so it doubles as an end-to-end read check — withContentPak: '%s' index read failed (attempt %d of 3)/index decode failed (attempt %d of 3) -- re-readingwarnings, and the asset read error now readsread of %u bytes at %u failed for '%s'.SYS_FileOpenRead/SYS_FileRead/SYS_FileSeek/SYS_FileCloserun under newSYSFILE_IO_LOCK/SYSFILE_IO_UNLOCK/SYSFILE_IO_SYNC/SYSFILE_IO_GUARD(aSysFileIoGuardRAII struct), andSYS_FileReadreads in 8 KB slices (kIoSliceBytes = 8 * 1024) releasing the lock between them, because holding it across a whole streaming chunk starved the mixer (which needs the lock every ~2 ms to top up audsrv) and glitched audio for the duration of every read. This also removed a doublerpc_packet_freeof the same packet from a third caller (null-packet crash insifrpc.c:318,_request_end). AudioManager:sStreamThreadRunstaysfalse; the comment now records the PS2 profiling behind that decision (UpdateStreamingSources()blocked the render thread ~310 ms every ~5 s; threading it fixed the hitch but produced audible glitching and doubled-up playback; the ring runs full at ≈68 rejects/s and ≈0 underruns, so the fault is in source management above the stream layer).
Android HTTP Backend (beta.17)
HttpBackend_Android.cpp(new;AndroidHttpBackend : public HttpBackendin an anonymous namespace, returned byCreatePlatformHttpBackend()underPLATFORM_ANDROID) drivesjava.net.HttpURLConnectionover JNI — not OkHttp, Cronet or libcurl. Rationale in the header: the Android link list carries no libcurl or OpenSSL, so HTTPS goes through the OS stack and its auto-updated CA store, the same OS-native policy as WinHTTP on Windows and libctruhttpcon 3DS. Three new public methods onPolyphaseActivity(called onsystem.mActivity->clazz, likeiterateDirFiles/setSystemOrientation):Object[] httpOpen(String verb, String url, String[] headerLines, byte[] body, int timeoutMs, int maxRedirects, boolean verifySsl)returning{ HttpConn connOrNull, Integer status, String errorCode, String errorMessage, String finalUrl, String[] headerLines };byte[] httpReadChunk(Object conn, int maxLen);void httpClose(Object conn); plus a private staticHttpConnholder (HttpURLConnection+InputStream) andinstallTrustAllForConnection(HttpsURLConnection).errorCode∈none / timeout / tls / badresponse / network / unknown, mapped byMapErrorCode()ontoHttpError::Timeout / Tls / BadResponse / Network / Unknown.- Supported. HTTPS via
HttpsURLConnection;HttpRequest::VerifySsl(false)installs a permissiveX509TrustManager+HostnameVerifieron that single connection instance only, never on the process-wideSSLContext; request headers marshalled asKey: Valuelines split at the first:; response headers enumerated withgetHeaderFieldKey(i)/getHeaderField(i), duplicates merged with", "inCopyHeaders()to match the libcurl backend'sHeaderCb(repeatedSet-Cookie);POST/PUT/PATCHbodies throughgetOutputStream()(bodies on other verbs are dropped);GetTimeoutMs()(default 10000) applied to bothsetConnectTimeoutandsetReadTimeout; redirects followed manually (setInstanceFollowRedirects(false), loop over 301/302/303/307/308, honoursGetMaxRedirects()default 5, crosseshttp↔https, which the built-in follower refuses; a redirect with noLocation→badresponse; the resolvedconn.getURL()→HttpResponse::SetFinalUrl);getErrorStream()fallback so 4xx/5xx bodies are readable; blocking onHttpClient.cpp's workerstd::threadwith aScopedJniThreadRAII attach/detach per request (safe because the worker is never already attached, unlike the UI thread); body pulled inkReadChunkSize = 64 * 1024chunks pollingcancelFlagbetween them (HttpError::Cancelled);GetMaxBodyBytes()(64 MiB) enforced inStreamBody()(HttpError::TooLarge). - Not supported. Download-to-file or streaming to disk (the body accumulates in
HttpResponse::MutableBody()), progress callbacks, streamed uploads (the request body is a materialisedstd::vector<uint8_t>), HTTP/2 or connection-reuse control, cookie jar, proxy configuration, client certificates. Cancellation granularity is one chunk; a stalled connection blocks until the read timeout.Initialize()fails (IsAvailable()false) withAndroid activity/JavaVM not available.outside an activity process.HttpBackend_Stub.cppaddsPLATFORM_ANDROIDto its negative guard. No manifest change was needed:INTERNETandACCESS_NETWORK_STATEwere already declared. VulkanContext.cpp: theVK_INSTANCE_CREATE_ENUMERATE_PORTABILITY_BIT_KHRblock is now#if PLATFORM_MAC. The constant comes fromvulkan_beta.hand the NDK'svulkan_core.hdoes not declare it, which broke the Android compile;portabilityEnumFoundwas only ever set in the Mac branch anyway.
Headless Service Mode (-serve) (beta.17)
-serve [port]implies-headlessand still requires-project <path>;ReadCommandLineArgstakes the port from the next argv when it does not start with-. NewEngineConfig::mHeadlessService(false) andmServicePort(7890, matchingNetworkModule); newPOLYPHASE_API bool IsHeadlessService()(IsHeadless() && mHeadlessService).LoadProject's native-addon gate changed from!IsHeadless()to!IsHeadless() || IsHeadlessService()so a service loads native addons before scenes deserialize — otherwise addon node types (VideoPlayer3D, for example) are unknown and collapse to their nearest registered parent,Node3D, corrupting the scene's type layout. A one-shot-headless -buildcook still skips them.EditorMaingained anIsHeadlessService()branch ahead of the one-shot cook path. It createsActionManager,BuildCache,EditorUIHookManager,PreferencesManager,NativeAddonManagerandAddonManager; setsNativeAddonManager::Get()->GetEngineAPI()->editorUI = EditorUIHookManager::Get()->GetHooks()so addon-registered REST routes and node/asset categories are live;LoadProject(...)thenActionManager::CheckProjectNeedsUpgrade()/UpgradeProject()(auto-upgrade to the UUID asset format);GetEditorState()->Init()(every GFX call it reaches isIsHeadless()-guarded) and an explicitSetAssetDirectory(AssetManager::Get()->FindProjectDirectory(), false), because plainLoadProjectleavesGetAssetDirectory()null and a nullAssetDir*makesRegisterAssetproduce an emptymPath(assets register in memory and never reach disk);ControllerServer::Create()+Start(mServicePort); a main loop ofUpdate(),NativeAddonManager::TickAsyncBuilds(),TickEditorAllPlugins(dt),ControllerServer::Tick(),SYS_Sleep(4); orderly teardown of all six managers andShutdown(). Net effect: an external process can drive the editor's REST API — imports, cooks, scene queries — against a project for as long as the process is up, with no interactive editor and no GPU.- Headless guards (the
Fixed Linux addon loadingcommit; whitespace-ignored it is 65 added lines, every one the same class of bug — headless code driving paths that need a window, an ImGui context or a Vulkan context):EditorProgress::Begin()andDoPumpUnthrottled()early-out (ImGui::GetTime()andRenderer::Render()would fault; leavingmProgressActivefalse makes every other entry point inert);EditorImguiDraw()returns (ImGui::GetIO()hard-asserts with no current context);EditorState::Update()returns (Viewport3D::ShouldHandleInputasserted every frame);ThemeModule's constructor gate ismShowTheming && !IsHeadless()andLoadSettings()skipsEditorTheme::ApplyTheme(ImGui::GetStyle()on a nullGImGui);Engine::Update()wraps the per-world render loop in!IsHeadless()(GFX_MakePerspectiveMatrixdereferences the nullVulkanContextsingleton);SYS_Update()on Linux returns (xcb_poll_for_eventon the null connectionSYS_Initializenever created);AUD_Update()returns whensSoundDevice == nullptr(snd_pcm_avail(nullptr)hard-asserts inside libasound when there is nodefaultALSA device, routine on a server or an SSH session). - Import fixups headless.
TextureImportFixupModal::EnqueueunderIsHeadless()applies the non-destructiveApplyPadand logs a warning instead of queuing a row nobody will answer (the stranded row leftmPixelsempty and madeActionManager::ImportAssetskip its save — a silent pixel-less import);SoundWaveImportFixupModalauto-resamples tokTargetSampleRateviaApplyResample(row)and warns. - Exported to addons.
ActionManager::OpenScene(Scene*),ImportAsset(...),ImportAssetCombined(...)andRetargetAnimation(const RetargetAnimationOptions&)arePOLYPHASE_API: the per-asset importers let an addon drive a conversion pipeline withoutImportScene(which ends inOpenEditSceneand needs a fully initialized editor), andRetargetAnimationtakes an explicitmTargetDirand never readsEditorState::GetAssetDirectory(), so it is callable from a headless cook.
Character Creator (beta.17)
Tools > Create > First Person Character/Third Person Character(EditorMainMenu.cpp, above the Version Control submenu; disabled withStop play-in-editor first./Open a project first.tooltips). NewEditor/CharacterCreator/CharacterCreatorDialog.{h,cpp}(OpenCreateCharacterDialog(CharacterRigType),DrawCharacterCreatorDialogs()called once per frame fromEditorImguiDrawbesideDrawScriptCreatorDialogs) andCharacterRigBuilder.{h,cpp}. The dialog (Create First Person Character###CreateCharacterDialog) is a plainImGui::Beginwindow rather thanBeginPopupModal, because a modal blocks input to every other window and breaksAssetRefPickerautocomplete and asset-browser drag-drop. Fields: Name (prefilledFirstPersonPlayer/ThirdPersonPlayer; rejects/ \ : * ? " < > |); Skeletal Mesh (Polyphase::AssetRefPickerfiltered toSkeletalMesh; "Recommended" for third person, "Optional" for first); Fit mesh to height (scale the bind pose to exactlyHeight, off = authored scale 1); Height (m) (DragFloat0.3–10.0, default 1.8, live readoutCapsule: Height %.2f + 2 x radius %.2f caps = %.2f m total.plusEye height %.2f m.orCamera pivot at %.2f m, camera %.2f m behind.); Script (text field + combo:kBuiltInScripts[] = { "FirstPersonController", "ThirdPersonController" }labelled(engine), a separator, then every project script fromAssetManager::GetAvailableScriptFiles()de-duplicated, sorted,.luastripped; accepts aDRAGDROP_SCRIPTpayload from the Scripts panel); Create Scene asset ("reusable prefab; an instance is placed in the open scene", off = spawn directly) with Output Directory +Browse...(SYS_SelectFolderDialog; must be inside the project asset root; defaults to the asset browser's current directory; resolved absolute path shown). Escape closes;Createis disabled with the exact blocking reason.- What is built.
CharacterRigBuilder::BuildRig(spec, outError)touches no ImGui, so the REST controller or an addon can drive it. It builds a node hierarchy, not a skeleton:<Name>Capsule3D(EnableCollision(true),EnablePhysics(false),EnableOverlaps(true),SetCollisionGroup(ColGroup0),SetCollisionMask(ColGroupAll),AddTag("Player")— a collider, not a rigid body, because the controllers move viaSweepToWorldPosition+SetWorldPosition); first person: childCamera(Camera3D) at eye height, Main Camera; third person:CameraPivot(Node3D) at chest height withCamerabehind it;Mesh(SkeletalMesh3D; always for third person, only when a mesh is picked for first);Controller(Node) carrying the script.ComputeMetrics(spec)→CharacterRigMetrics, shared with the dialog preview: height clamped[0.3, 10.0],radius = clamp(0.2H, 0.1, 0.6),capsuleSegment = max(H - 2r, 0.05)(Bullet's capsule height is the cylinder only),rootY = 0.5H(feet ony = 0),eyeLocalY = 0.9H - rootY,pivotLocalY = 0.75H - rootY,cameraDistance = 2.5H,followCamHeight = 1.5H(the Lua defaults of 10 / 4 assume a large demo world). Mesh placement uses the newSkeletalMesh::GetBindPoseAABB()rather thanGetAABB()(which is padded by Bounds Scale and would both mis-scale and float the mesh) at-rootY - minY * scale, so feet sit on the capsule bottom for both feet-origin and centred-origin rigs; an animation namedIdlebecomesDefault Animation.controller->SetScriptFile(...)instantiates the Lua class immediately, which populates the property list (errors:Script 'X' failed to load. It must define a table named after the file./Script 'X' was not found in <Project>/Scripts or Engine/Scripts.); thencolliderandcameraare set by name for both rigs, pluscameraPivot,mesh,cameraDistance,followCamHeightfor third person (the controllers auto-resolve collider/camera inStart(), but explicit refs survive renames;cameraPivot/meshare never auto-resolved). - Outputs.
SpawnRigIntoScene(spec, parent, feetPos, outError): undo groupCreate Character, deep-clone viaActionManager::EXE_SpawnNode(remaps script node refs to the clone's own children),EXE_AttachNodeor promote to root, select — one undo step; the rig is built and validated before the group opens so a bad script never leaves a half-made undo entry; refuses to parent inside an instanced scene (Cannot add nodes inside an instanced scene...); warns when the scene already had a main camera (The first one found wins at Play.).SaveRigAsScene(spec, dir, outError): writes<Name>.oct(uniquified viaEditorAddUniqueAsset) into theAssetDir,Scene::Capture, save; refuses the engine asset directory; then, with a scene open, places an instance at the focus point under undo groupPlace Character(SpawnBasicNode(BASIC_SCENE, ...)), or with none open callsActionManager::RequestOpenScene(stub)(deferred, since opening a scene must not happen inside the ImGui frame). Placement isEditorGetFocusPosition()withyforced to 0.ResolveOrCreateOutputDir(userPath, outError)creates on disk (CreateDirectoryRecursive) and in theAssetDirtree (AssetDir::CreateSubdirectory) behind a case-insensitive containment check; alsoToProjectRelativeAssetPath,GetProjectAssetRoot,CharacterRigSpec { mType, mName = "Player", mHeight = 1.8f, mSkeletalMesh, mFitMeshToHeight, mScriptFile, mTagAsPlayer = true, mMainCamera = true },enum class CharacterRigType : uint8_t { FirstPerson, ThirdPerson }. SkeletalMesh. NewAABB GetBindPoseAABB() const { return mAABB; }. Fix:Create()now callsComputeBounds()whenmNumVertices > 0 && mVertices.size() >= mNumVertices, beforeGFX_CreateSkeletalMeshResource, becauseLoadStreamrestored the bounding sphere but notmAABB, so any skeletal mesh loaded from disk reported the default unit box fromGetAABB()(runtime, all platforms). Build files:Engine.vcxproj+.filters(four new files),Source/Editor/CharacterCreatoradded toSOURCESin both the shared-editor and static-editor branches ofMakefile_LinuxandMakefile_Mac,.llm/Editor.mdgains aCharacterCreator/row.
GameCube / Wii — USB Gecko Log and GDB (beta.18)
- USB Gecko log (
System_Dolphin.cpp). Newstatic s32 sGeckoChan = -1andInitUSBGecko(), called fromSYS_Initialize()beforeVIDEO_Init()so boot-time logs reach the wire. It probes EXI channel 1 (memory card slot B, since the engine's save path uses slot A) then 0 withusb_isgeckoalive().SYS_Logmirrors each line with the non-blockingusb_sendbuffer()(newline sent separately), so an unplugged cable or a closed terminal can never hang the game. The whole probe is compiled out under#if !POLYPHASE_GDB, because in GDB builds the stub owns the EXI channel and raw log text would corrupt GDB remote packets. SYS_Logfixes. ThePLATFORM_WII/elsesplit (vsnprintf+SYS_ReportvsSYS_Reportv) was backwards relative to its own comment. It now formats once intochar logBuffer[256], bails on a negativevsnprintfreturn, clamps on truncation, and callsSYS_Report("%s\n", logBuffer)— the buffer as an argument, not the format string, so a log line containing a stray%is no longer re-expanded (a format-string bug).SYS_Reportvonly exists in the GameCube's libogc2; the unified path works on libogc v1 (Wii) and libogc2 (GameCube).- GDB over Gecko. New make variable
POLYPHASE_GDB=1inEngine/Makefile_GCN,Engine/Makefile_Wii,Standalone/Makefile_GCN,Standalone/Makefile_Wii:GDB_CFLAGS = -Og -gdwarf-4 -gstrict-dwarf -DPOLYPHASE_GDB=1(else-O2;CFLAGSchanged from the hardcoded-g -O2to-g $(GDB_CFLAGS)),GDB_LIBS = -ldbspliced intoLIBSbefore-logcin the Standalone makefiles; the block is duplicated into the engine makefiles so building the engine library directly with the flag produces a matching archive rather than a DWARF-5 /-O2mix (command-line variables propagate into theMakeEnginesub-make). devkitPPC pins GDB at 7.6.2, which cannot parse the DWARF-5 modern GCC emits by default, hence-gdwarf-4;-O2inlines away most of what you would step through, hence-Og.Standalone/Source/Main.cppOctPreInitialize()callsDEBUG_Init(GDBSTUB_DEVICE_USB, POLYPHASE_GDB_CHANNEL)(new define, default1= slot B) followed by_break()under#if PLATFORM_DOLPHIN && POLYPHASE_GDB— the earliest hook the game owns, beforeReadEngineConfigandInitialize(), so a breakpoint set from GDB can catch engine startup. Usage:make -f Makefile_Wii clean && make -f Makefile_Wii POLYPHASE_GDB=1,powerpc-eabi-gdb Build/Wii/Polyphase.elf,target remote \\.\COM4(or/dev/ttyACM0on a CDC clone). Objects carry no dependency on the flag, somake cleanwhen toggling; libogc2 itself needs the same two DWARF flags for its own frames to resolve.
Addons Manager — Update, Remove, Preflight
- Deferred pipeline. Buttons only queue ids into
EditorState::mPendingAddonInstalls / mPendingAddonUninstalls / mCheckAddonUpdatesAtEndOfFrame;EditorMaindrains viaAddonsWindow::ProcessPendingInstalls / ProcessPendingUninstalls / ProcessPendingUpdateCheckunderEditorProgress, because a synchronous curl inside an ImGui frame froze the editor with no feedback. - Update tracking. New
AddonInstallSourceandAddonUpdateStatus(NotChecked / UpToDate / NewCommits / NewVersion / Pinned / NoSource / Error);Addon::mBranch(defaultmain);InstalledAddongainsmBranch,mCommit,mCommitDate,mPinned,mIsStandalone.AddonManager::FetchLatestCommit()querieshttps://api.github.com/repos/<owner>/<repo>/commits?sha=<branch>&per_page=1[&path=<id>](10 s timeout, memoised);CheckForUpdates(outSummary)refreshes registries first;GetUpdateStatus()combines registry version andmUpdatedstamp with the branch-head SHA as primary, with a date fallback for pre-tracking installs.DownloadAddonrecords the branch head before fetching the archive so the recorded commit is never newer than the files; 40-hex refs are markedmPinned.UninstallAddon()also removesIntermediate/Plugins/<id>and warns on locked files. - UI.
Check Updates(Browse),Check for Updates+Update All (N)(Installed),Removeon Browse rows, cards and details;DrawUpdateStatusLabel("New commits", "New version", "Up to date", "Check failed", "Pinned", "No source", "Not checked"). TheUpdate Addonmodal warns that local edits are lost and flags folders that are their own git repo. TheRemove Addonmodal lists transitive installed dependents (removed first) and local-only dependents (left in place), notes when a project restart is needed, and is blocked during play-in-editor. - Dependencies.
AddonDependencyResolver::ReadPackageVersion / ListPackageIds / CollectDependents(reverse-graph post-order DFS, cycle-safe); a registry miss at project load triggers a one-timeRefreshAllRepositories()before declaring a dependency missing.NativeAddonState::mMissingDependencies / mFixHint / mFixUrl;RefreshMissingDependencies()blocks build and load until deps exist;DiscoverAddonPackage(id),ForgetAddon(),RemoveNativeAddonsWithProjectRestart()(unload → forget →UninstallAddoninside the close window, never rebuilds). - Build preflight.
RunBuildPreflight()checks deps on disk, engine include dirs, Vulkan headers and (Windows)Polyphase.lib/PolyphaseEditor.lib+Lua.lib;ClassifyBuildFailure()maps compiler output to hints.ResolveVulkanIncludeDir()probesVULKAN_SDK,<engine>/External/Vulkan/include,C:/VulkanSDK/*/Include,/usr/include,~/VulkanSDK/*/{x86_64,macOS}/includeand Homebrew; generatedbuild.bat/build.sh/ VS / VS Code / CMake configs use the resolved path (CMake emitsFATAL_ERRORwhen none is found). The problem modal is nowNative Addon Problem, with a "How to fix" section,Open download page,Build Dependenciesand per-dependencyInstall <id>buttons. Shadow-copy delete retries are paced (kRetryIntervalSec = 5,kMaxRetries = 6). - Distribution.
stage_distribution.pystages Vulkan headers (vulkan,vk_video, LICENSE) fromVULKAN_SDKand the prebuiltExternal/mbedtls/lib-wiiandlib-gcnarchives (fixescannot find -lmbedtlsin installed Wii/GameCube packaging);release.ymlfails if the stagedvulkan.his missing;package_windows_sdk.pyships the headers in the SDK zip;External/Vulkan/gitignored.
Import and Editor QOL
SoundWaveImportFixupModal(Sound Import - Non-standard Sample Rate): per-row or bulkResample to 44100 Hz,Keep,Cancel Import; triggered when the rate is neither 44100 nor 22050 because Android hard-locks 44.1 kHz and console cooks assume it. Backed by newAUD_ResamplePcm(src, srcSize, bits, channels, srcRate, dstRate, outPcm, outNumSamples)(AudioResample.h/.cpp, linear, 8-bit unsigned / 16-bit signed, 1–2 channels).SoundWave::ParseWav/WavInfoshared byImportandLoadFromMemory; newGetNumFrames(); the inspector shows duration, PCM size and a non-standard-rate warning.Batched imports.
ActionManager::RequestImportAssets()/ImportPendingAssets()run non-mesh imports at end of frame under a cancellableImporting Assetsmodal (file dialog andFileDropImportModal).EditorProgress::SetStatuscalls added across addon download, extract, install, registry refresh, dependency fetch and asset save; fixup modals and theAsset Name Clashpopup wait while progress is active.Scene::Instantiatereports progress only for the outermost scene.Loose-file multi-select in the asset browser (Ctrl/Shift), type-coloured rows,
Packaging (N files)/Delete (N files)context actions; the inspector applies flags to all selected.InputMap::SetKeyboardEmulationEnabled / IsKeyboardEmulationEnabled(defaulttrue, persisted askeyboardEmulationEnabled) with aKeyboard -> Gamepad Emulation Enabledcheckbox in the Input Map window; gates all keyboard → gamepad button and axis emulation.BuildDependencyWindowaddsCheckMakerom(),CheckVulkanSDK(), and validates thatDEVKITPPC/DEVKITARMpoint at a folder containingwii_rules/3ds_rules(cygpath-converted on Windows);GetDevkitproPath()falls back to/opt/devkitpro.LaunchersModule::BuildAdbLogcatCommandgains anosascriptTerminal.app variant.*Logging toggle is live (beta.18).*
EngineConfig::mLoggingwas only copied into the log gate atInitializeLog(), so the App Settings > Runtime > Logging checkbox took effect at the next editor launch and a project's ownConfig.inisetting was ignored for the whole session (the one sync at engineInitialize()runs before any project is open).AppSettingsWindow::DrawRuntimeSectionnow callsEnableLog(config->mLogging)on change, andLoadProjectcallsEnableLog(sEngineConfig.mLogging)right afterReadEngineConfig()(#if EDITOR, skipped whenIsHeadless()so cooks keep logging).LoadProjectalso logsLoading project '%s'.*Scene tab title after duplicating a scene file (beta.18).* An asset copied or renamed outside the editor keeps its old serialized
Name, so anything showingAsset::GetName()— the scene tab — showed the original.AssetManager::LoadAssetnow callsstub.mAsset->SetName(stub.mName)afterEnsureUuid()when the loaded name differs from the stub (#if EDITOR).
C# Scripting (PolyphaseSharp)
- Gameplay scripts can now be written in C#. Nothing changes at runtime: C# sources are transpiled ahead of time to ordinary engine Lua, so scripts run on the same Lua 5.3 (
LUA_32BITS) VM the engine already ships on every platform — desktop, Android, Web, PS2, PSP, 3DS, Wii/GameCube, Dreamcast — with no .NET runtime in the shipped game. The .NET SDK (8+) is an authoring-time dependency only; generated.luafiles sit next to the sources, so teammates who never touch C# never needdotnet. New tool:Tools/PolyphaseSharp/(polyphasesharp, net8.0,InvariantGlobalization).- Pipeline.
Scripts/CSharp/Foo.csplus the engine API sources fromTools/PolyphaseSharp/Polyphase.Engineare compiled by Roslyn (Microsoft.CodeAnalysis.CSharp5.6.0) inAnalyzer.Compile;CS*errors surface first, then the analyzer discovers script classes,[Property]fields,[Button]methods, overridden lifecycle methods and public methods, and runs subset lints.SourceRewriterturns each[Property]field into anexternproperty whose@CSharpLua.Get/Settemplates read and write{this}.__node.<Name>(the node uservalue is the single source of truth) and wraps namespace-less files in the default namespaceGameso CSharp.lua's global publishing cannot collide with engine script tables. The rewritten sources go through the vendoredCSharpLua.Compiler;WrapperEmitterthen produces the final per-file.lua: a-- ~PolyphaseSharp~marker line,Script.Require("CSharp/CSharpCore")plus aScript.Requireper cross-file class dependency, the module body,CSharpCore.Finalize({...})in inheritance-depth order, and the engine wrapper table. - Runtime bundle.
CoreSystemBundlerwrites oneScripts/CSharp/CSharpCore.lua: the CSharp.lua CoreSystem BCL runtime (comment-stripped byLuaMinifier, hot-reload guarded by__POLYPHASE_CSHARP_CORE__), the transpiled Polyphase API module, and the glueCSharpCore.New(cls, node)/CSharpCore.Finalize(ordered). The bundle is trimmed to modules whose usage tokens appear in the generated code (akDroppabletable withKeptBydependencies); dropped modules are logged. Orphaned generated files (marker present, no longer produced) are deleted; hand-written Lua is never touched. - Authoring. Derive from
Polyphase.Script(any node) orPolyphase.Script3D(transform nodes). File name must equal the class name; one script class per file. Lifecycle overrides:Create(before serialized props apply),Awake,Start,Tick(float),EditorTick(float),Stop,Destroy,BeginOverlap(Node),EndOverlap(Node),OnCollision(Node, Vector3, Vector3). Bare calls (AddRotation,Position,Name,AddTag) act on the attached node, likeself:in Lua;Nodereturns the handle (typedNode3DonScript3D),Worldthe owning world. The generated file emitsRotator = {}andfunction Rotator:Create()which applies property defaults then constructs the companion viaself.__cs = CSharpCore.New(Game.Rotator, self); only the lifecycle methods the class actually overrides get forwarders, and overlap/collision forwarders dropthisNode.[Property]/[Button]members produce aGatherProperties()returning{ name, type = DatumType.X, display_name, tooltip }entries ([Button]→DatumType.Function). Every public, non-overloaded instance method also gets a wrapper forwarder so Lua or other C# can callnode:Method(args); overloads emit warningPS2003and no slot. Statics and singletons work; cross-file references get automaticScript.Require. - Escape hatch.
Lua.GetGlobal / SetGlobal / Get / Set / Call(target, method, up to 6 args) / As<T>. Typed façades areNode3Dsubclasses withexternmembers carrying/// @CSharpLua.Templatecomments. - Engine API surface (
Polyphase.Engine, namespacePolyphase, netstandard2.1, extern-template only):Script,Script3D(lifecycle plusName,Position/Rotation/ScaleandWorld*variants,AddRotation,AddWorldRotation,RotateAround,LookAt,GetForward/Right/UpVector,FindChild, tags,EnableTick,DestroyNode,EmitSignal);Node,Node3D,Primitive3D(SweepToWorldPosition,EnableOverlaps/Collision/Physics),Camera3D(FieldOfView),SkeletalMesh3D(PlayAnimation,StopAnimation,StopAllAnimations,IsAnimationPlaying,QueueAnimation),HitResult,World(GetRootNode,RayTest,FindNode);Vector3(maps to engineVec, reference semantics, operators,Dot/Cross/Lerp/Rotate/Distance,Clone,Normalize),Color;Input,Key,Gamepad(constants read from Lua tables at runtime, never baked),Mathf,Log,Lua; attributesPropertyAttribute(Display) andButtonAttribute(Title,Tooltip). - CLI.
polyphasesharp --scripts <Project>/Scripts/CSharp [--scripts-root <dir>] [--lua-roots <a;b>] [--check] [--no-trim] [--verbose]. Exit codes 0 ok, 1 compile/validation error, 2 usage. Diagnostics arefile(line,col): error|warning CODE: msg. Codes:PS0001API sources missing;PS1001file/class name mismatch;PS1002multiple script classes;PS1003generic script;PS1004constructor;PS1005unsupported[Property]type;PS1006non-literal initializer;PS1007name collides with hand-written Lua (checked across--lua-roots, i.e.Engine/Scriptsand addonPackages/*/Scripts);PS1008/PS1009property field shape;PS1010duplicate basenames;PS1011non-identifier basename;PS1012bad[Button]signature; warningsPS2001(long/ulong/decimal),PS2002(async),PS2003(overloads). - Vendored dependency.
Tools/PolyphaseSharp/External/CSharp.lua/(83 files) is yanghuan/CSharp.lua pinned at6dcc5b155…(2026-07-29), Apache-2.0 — the compiler library plusCoreSystem.Lua; upstream launcher, tests and docker are not vendored.VENDOR.mdrecords two mandatory local patches (markedPOLYPHASE PATCH):LuaSyntaxNodeTransform.csVisitPropertyOrEventIdentifierNamebinds the implicitthisreceiver for property templates eagerly (upstream NRE'd inLuaRenderer.Renderon barePropaccess, which the entire bare-call API depends on);CoreSystem/Core.luaaddsSystem.getRegisteredModuleNames()soCSharpCore.Finalize()canSystem.initonly the types one generated file registered, which per-file hot reload requires. - Limitations (v1). 32-bit Lua VM:
doubleisfloat;long/ulong/decimalare unrepresentable. No threads;asynchas no scheduler. No reflection.[Property]types areint,short,byte,float,double,bool,string,Vector3,Color,Node,Node3Dand Node-derived; initializers must be literals ornew Vector3/Color(<literals>); no arrays. Binary NuGet packages are unsupported by design (IL cannot run) — vendor source instead. Debugging is at generated-Lua level; no C# source maps. Hot-reloadingCSharpCore.luaitself requires a game restart. - Tests.
Tests/run_tests.ps1builds the tool, copiesTests/Sample/Scripts/CSharp/Rotator.csintoTests/work/, transpiles it, builds a Lua 5.3 interpreter once from the engine's ownExternal/Lua(withTests/lua_main.cas driver) soLUA_32BITSsemantics match shipping platforms, and runsTests/harness.lua, which stubsDatumType,Log,VecandScript.Require, fakes a node withRotatoras its metatable, and assertsCreateseeds__csand defaults,GatherPropertiesyields two typed entries, threeTick(0.25)calls accumulate 67.5° of rotation, togglingEnabledthrough the uservalue stops it, and re-running the chunk (hot-reload simulation) works. The interpreter build is Windows/MSVC-only (vcvars64.bat); the tool itself is portable.
- Pipeline.
- Editor integration —
CSharpManager(newEngine/Source/Editor/CSharp/CSharpManager.{h,cpp}, singletonCSharpManager::Get()).EnableForProject()setsmCSharpScripting = trueand callsWriteEngineConfig()itself (so the flag persists even if the settings window is closed without Save), scaffolds<Project>/Scripts/CSharp/viaEnsureWorkspaceFiles(), drops a sampleSpinnerCS.cs(aScript3Dwith a[Property] Vector3 AngularVelocity = (0, 90, 0)) only into an empty folder — deliberately not namedRotatorbecause script class names are global andEngine/Scripts/Rotator.luaalready owns that name — and force-reprobes dotnet, loggingwinget install Microsoft.DotNet.SDK.8/sudo apt install dotnet-sdk-8.0guidance when missing.EnsureWorkspaceFiles()writesGame.csproj(netstandard2.1,LangVersion latest,Nullable disable, obj/bin redirected to../../Intermediate/CSharp/,NoWarn CS0626;CS0824;CS0169;CS0649, engine API source-included via a<Compile Include=".../Tools/PolyphaseSharp/Polyphase.Engine/Polyphase/**/*.cs" Link="EngineApi/..."/>so no prebuilt DLL is required) and a fixed-GUID single-projectGame.sln. Idempotent — projects enabled by older builds pick upGame.slnretroactively onOpenIde().LoadProjectrewrites thePOLYPHASE_ENGINE_API-tagged<Compile Include>line so IntelliSense survives an engine or project move.CheckDotnet()runsdotnet --version, requires major ≥ 8, and caches the result.EnsureToolBuilt()runsdotnet build PolyphaseSharp.csproj -c Release --nologo -v qonly when the newest.cs/.csproj/.lua/.xmlunder the tool root is newer thanbin/Release/net8.0/polyphasesharp.dll.Transpile(checkOnly, outLog)invokes the tool with--scripts <Scripts/CSharp> --lua-roots "<Engine>/Engine/Scripts;<Project>/Packages"so a C# class shadowing an engine or addon Lua script fails the transpile instead of silently losing the duplicate at packaging time; output routes toLogError/LogWarning/LogDebugby line.- UI.
Tools > CSharpmenu (project loaded only):Build C# Scripts(Ctrl+R; transpile thenRequestReloadAllScripts()+InvalidateScriptPickerCache()),Open C# Solution,Check Dependencies, andEnable C# for This Projectwhen not yet enabled. App Settings > Runtime:C# Scriptingcheckbox; when enabled but dotnet is missing shows ".NET SDK 8+ not found" with anInstallbutton (Windows runs the winget command detached; Linuxxdg-opens the download URL). Disabling only stops future transpiles; generated.luastay in place. Create Project dialog:Scripting:radio (Lua/C#,mProjectType0 / 2); the old C++ type (1) stays hidden. Build Dependency window: new.NET SDKcheck ("Required only for C# scripting (8.0 or newer)"). - IDE preference. Preferences > External > Editors gains a "C# IDE" section:
EditorsModule::CSharpIdeenum (VisualStudio= 0 default,VSCode= 1,Custom= 2), persisted ascsharpIde,csharpIdePath,csharpIdeArgs(default{editor} {folder}; placeholders{editor},{solution},{project},{folder}).OpenCSharpWorkspace(): VS Code opens the folder via thecodeCLI (a.csprojopened as a file gives no IntelliSense); Visual Studio resolvesdevenv.exeonce viaExternal\vswhere\vswhere.exe -latest -property productPathand launches it withSYS_SpawnDetachedExecutable— explicitly notstart "" Game.sln, because the file association frequently points at VS Code on machines that have it; falls back to the OS association with a warning if vswhere returns nothing. - Scripts panel. A generated
.luawith a sibling.cs(HasSiblingCSharpSource, computed on refresh not per frame) shows as one merged row tagged[C#]; its context menu gainsOpen C# Sourceand relabels the Lua entriesOpen Generated Lua in External Editor / Script Editor. Ctrl+R (Edit_ReloadScripts) transpiles first whenmCSharpScriptingis set. - Auto-transpile on save.
EngineConfig::mCSharpScripting(defaultfalse, persisted asCSharpScripting=).FileWatcher::WatchDirectory(dir, recursive=true, extensions={".lua"})gains an extension filter (empty = all files). With C# on,LoadProjectwatchesScripts/for{".lua", ".cs"}; any.csevent (including deletes) schedules a debounced transpile (400 ms) serviced inEngine::Update()(non-headless only). The generated.luawrites then hot-reload through the normal per-file path. - Packaging.
BuildPhase1transpiles before script gathering so generated.luaflow through the embedded and loose-copy paths; a failed transpile aborts the build..cs/.csprojare stripped from the packagedScripts/tree (mirrors the LuaPanda strip).BuildCache::GatherScriptFilesadds.csfiles (skippingobj//bin/) as ordinaryFileEntryrows so editing a.csinvalidates the cache without a manifest format change. Makefile_Linux:Source/Editor/CSharpadded toSOURCESfor both the shared-libraryPolyphaseEditorand staticEngineEditortargets.
Networking — WebSocket Client
- New WebSocket client subsystem (RFC 6455, client-only), a per-frame-pumped, callback-or-polling client parallel to
Http::. All new files live flat inEngine/Source/Network/on purpose: a new subdirectory would have to be added to every engine console Makefile and to every already-shipped build-target package Makefile; flat files are picked up automatically everywhere exceptEngine.vcxproj(updated). Files:WebSocketTypes.h,WebSocketClient.h/.cpp,WebSocketFraming.h/.cpp,WebSocketHandshake.h/.cpp,WebSocketTransport.h(internal),WebSocketWss_Windows.cpp,WebSocketWss_Linux.cpp,LuaBindings/WebSocket_Lua.h/.cpp.- C++ API.
namespace WebSocket { Initialize(); Shutdown(); Tick(); bool IsAvailable(); const char* GetMissingDependencyMessage(); std::shared_ptr<WsConnection> Connect(const WsConnectOptions&, std::string& outError); }. AbstractWsConnection:GetState(),SendText(const char*, uint32_t),SendBinary(const uint8_t*, uint32_t),Close(uint16_t code, const char* reason),SetOpenCallback/SetMessageCallback/SetErrorCallback/SetClosedCallback,GetAvailablePacketCount(),TakePacket(WsMessage&),GetSelectedProtocol(),GetCloseCode(),GetCloseReason(),WasDowngraded(). Callback aliasesWsOpenCallback = void(),WsMessageCallback = void(const uint8_t*, uint32_t, bool binary),WsErrorCallback = void(const char*),WsClosedCallback = void(uint16_t, const char*, bool wasClean). - Types.
enum class WsState : uint8_t { Connecting=0, Open=1, Closing=2, Closed=3 }(matches browserreadyState);enum class WsError { None, BadUrl, NoTransport, Dns, Connect, Handshake, Protocol, TooLarge, Transport }+WsErrorToString();WsUrl,WsConnectOptions { mUrl, mProtocols, mHeaders, mMaxQueuedBytes },WsMessage { mData, mBinary }. Constants:WS_CLOSE_NORMAL 1000,WS_CLOSE_GOING_AWAY 1001,WS_CLOSE_PROTOCOL_ERROR 1002,WS_CLOSE_TOO_LARGE 1009,WS_CLOSE_ABNORMAL 1006(synthesized locally, never on the wire),WS_DEFAULT_MAX_QUEUED_BYTES1 MiB,WS_MAX_MESSAGE_BYTES16 MiB,WS_RECV_CHUNK_SIZE 8192,WS_CONNECT_TIMEOUT_SECONDS 15,WS_CLOSE_GRACE_SECONDS 5(peer never answers our Close → dropped,wasClean=false). - Three-arm compile seam.
#if !defined(POLYPHASE_PLATFORM_ADDON)real implementation;#elif defined(POLYPHASE_WS_PROVIDED_BY_ADDON)nothing (the build-target package supplies the wholeWebSocket::namespace and its ownWsConnectionsubclass, e.g. the web packages'Runtime/Web/Ws_Web.cpp);#elseself-contained stubs (IsAvailable()→ false,Connect()→ null, "WebSocket is not supported on this platform."). Keying the middle arm on an opt-in define rather thanPLATFORM_WEBmeans every pre-existing build-target package (PSP, Dreamcast, …) keeps linking with zero edits. - Two transport flavours (
WebSocketTransport.h). Byte transports (ws://overNET_stream sockets) hand raw bytes up and the engine runs handshake and framing; message transports (WinHTTP, libcurl, browser) own handshake/framing and exchange whole messages (SendWholeMessage— deliberately notSendMessagebecause<windows.h>macro-expands it). Per-platform secure hooks:WsIsSecureSupported(),WsSecureUnavailableMessage(),WsCreateSecureTransport(). - Platform coverage. Windows: ws + wss (WinHTTP WebSocket API, Windows 8+; receive and send worker threads push into a mutex-protected handoff queue drained by
Tick(); linkswinhttp.lib). Linux: ws + wss when libcurl ≥ 7.86 is present (dlopen'dlibcurl.so.4/.so/-gnutls.so.4/.so.3,CURLOPT_CONNECT_ONLY=2,curl_ws_recv/curl_ws_send; only the handshake blocks, on a one-shot joined thread). Android, 3DS (soc:u,FIONBIO), GameCube/Wii (libogc/libogc2): ws only. Web (web.webgl2/web.webgpu): supplied by the package viaPOLYPHASE_WS_PROVIDED_BY_ADDON. Other addon targets: stub arm. The core has no exceptions, RTTI or threads (consoles build-fno-exceptions -fno-rtti); only the two desktop wss backends thread, and nothing off the main thread touches Lua. - Framing.
WsEncodeFrame(always FIN, always client-masked with a fresh xorshift32 key; 7/16/64-bit lengths),WsEncodeClose(reason capped at 123 bytes), incrementalWsDecoder(Feed,Next(Event&),SetMaxMessageBytes; rejects RSV bits, fragmented or oversized control frames, high-bit lengths, stray continuations, unknown opcodes; reassembles fragments). Self-containedWsSha1()andWsBase64Encode()so no crypto dependency is dragged onto console toolchains. - Handshake.
WsParseUrl()(ws/wss only, default ports 80/443, fragment stripped, rejects userinfo@and IPv6 literals),WsBuildHandshakeRequest()(HTTP/1.1 GET upgrade,Sec-WebSocket-Version: 13, CR/LF header-injection guard),WsParseHandshakeResponse()→WsHandshakeResult { NeedMoreData, Ok, Failed }(requires 101,Upgrade: websocket,Connectioncontainingupgrade, exactSec-WebSocket-Accept; refuses anySec-WebSocket-Extensions; 32 KiB header cap). - State machine. Ordering is always
Open → Message* → Closed; a failure before Open yieldsError → Closed(1006, "", false). Ping is answered inside the transport and never surfaced. The incoming queue is only used when no message callback is set; overflow in either direction logs once per connection and drops.FinishClosed()releases all callbacks, breaking the registry→closure→userdata→connection cycle Lua's GC cannot collect.WebSocket::Tick()locks and compacts weak refs before pumping so a callback callingWebSocket.Connectis safe;Shutdown()closes live connections with1001 "Engine shutting down". - Engine wiring.
WebSocket::Initialize()afterHttp::Initialize(),WebSocket::Tick()underSCOPED_FRAME_STAT("WebSocketTick")afterHttp::Tick(),WebSocket::Shutdown()beforeNET_Shutdown().WebSocket_Lua::Bind()registered inLuaBindings.cpp. - New
NET_primitives (Network.h, all five backends: Windows/Linux/Android/3DS/Dolphin).NET_SocketConnectAsync(SocketHandle, ip, port)(leaves the socket non-blocking),NET_SocketConnectPoll(SocketHandle)(-1 fail / 0 pending / 1 connected viaselect+SO_ERROR; Wii usesnet_pollPOLLOUT, GameCube usesnet_selectbecause libogc2 declares but never linksnet_poll),NET_SocketWouldBlock(SocketHandle, opResult)(takes the returned value because libogc reports errors as negated errno in the return rather than inerrno). Purely additive; addon-targetNetwork_*.cppstubs need no new symbols. - wss→ws auto-downgrade, per-build-target opt-in. Compile define
POLYPHASE_WS_DOWNGRADE_WSS=1rewriteswss://tows://, maps default port 443→80 (explicit port kept), logs a warning once per connection, andWasDowngraded()reports true. Default off everywhere; never for web targets (mixed content). - Lua API (global
WebSocket, metatablePolyphase.WsConnection).WebSocket.Connect(url [, options]) → conn | nil, errwith options{ protocols = {...}, headers = {name=value}, maxQueuedBytes = n };WebSocket.IsAvailable();WebSocket.GetMissingDependencyMessage(). Connection:SetOpenCallback(fn()),SetMessageCallback(fn(data, isBinary)),SetErrorCallback(fn(message)),SetClosedCallback(fn(code, reason, wasClean))(nil clears),SendText(str) → bool,SendBinary(str) → bool,Close([code=1000 [, reason=""]]),GetState(),GetAvailablePacketCount(),GetPacket() → data, isBinary | nil,GetSelectedProtocol(),GetCloseCode(),GetCloseReason(),WasDowngraded(). Global enumWebSocketState = { Connecting=0, Open=1, Closing=2, Closed=3 }. Userdata holds ashared_ptr<WsConnection>;__gcclears callbacks thenClose(1001)— dropping the handle closes the connection, matching Godot'sWebSocketPeer. Callback errors arelua_pcall-caught and logged as "WebSocket callback error". - Known limitations. Client only; no server, no
permessage-deflate, no HTTP/2, no IPv6 literals, nouser:pass@URLs; outgoing messages are never fragmented; text frames are not UTF-8 validated. DNS (NET_ResolveHost) is synchronous on every platform, soConnectwith a hostname stalls — use a numeric IP or connect at menu/load time. Dolphin: libogc'snet_connectmay still block if IOS ignoresFIONBIO. Linux wss: libcurl exposes no negotiated-subprotocol API inCONNECT_ONLYmode, soGetSelectedProtocol()echoes the first offered protocol; the handshake thread cannot be aborted (bounded by the 15 s connect timeout). Theheadersoption is native-only; web builds warn and ignore it.
- C++ API.
Tools/ws-echo/— dependency-free Node ≥ 16 echo server (node:http+node:cryptoonly; framing adapted from the WebGL2 target's relay).node Tools/ws-echo/echo.js [--host 127.0.0.1] [--port 9002] [-v]. Echoes text and binary byte-identically (including fragmented), answers Ping with Pong, negotiates subprotocolecho.v1, closes with4000 "requested"on textclose-me, and rejects unmasked client frames. Pairs withScripts/WsEchoTest.luain the WebSocket test project (logsWS-TEST PASS/WS-TEST FAIL <step>). Not a production server.
Animation
- Multi-section
SkeletalMesh— per-part material slots. NewSkeletalMeshSection { mName, mFirstIndex, mIndexCount, mBaseVertex, mVertexCount, mMaterial }andstd::vector<SkeletalMeshSection> mSectionson the asset. Accessor surface:GetNumSections,GetSection,GetSectionMutable,GetSectionMaterial,SetSectionMaterial,FindSectionIndex,GetSections. Importing withMeshImportMode::AsSingleObjectproduces one section per source primitive.ASSET_VERSION_SKELETAL_MESH_SECTIONS = 36; assets below that version load with one implicit"Default"section spanning the whole index buffer, so the backend draw loop stays unconditional.- Per-instance overrides.
SkeletalMesh3DgainsGetNumMaterialSlots,GetMaterialSlot,SetMaterialSlot,FindMaterialSlot, backed by a new"Section Material Overrides"vector property filtered toMaterial. Resolution order: component override, then the section's own material, then the legacy whole-meshmMaterialOverride, then the mesh default, then the renderer default. - Lua. On
SkeletalMesh:GetNumSections,GetSectionName,GetSectionMaterial,SetSectionMaterial,FindSectionIndex. OnSkeletalMesh3D:GetNumMaterialSlots,GetMaterialSlot,SetMaterialSlot,FindMaterialSlot. Slot arguments are Lua-1-indexed;GetMaterialSlotandSetMaterialSlotaccept either an integer slot or a section-name string;FindMaterialSlotandFindSectionIndexreturn0when not found. - Backends. One indexed draw per section on Vulkan, GX (Wii and GameCube), and C3D (3DS). The Vulkan shadow pass still issues a single whole-mesh depth draw. C3D requires per-section
BindMaterialbefore the matrix/UV/color-scale uniforms — the reverse order leaves the first section drawing on stale state. - Inspector. The
SkeletalMeshasset inspector gains aSections (%u)block listing[i] <name> (<n> tris)with a per-row material picker, aBones (%zu)count, and a collapsible bone-hierarchy tree.
- Per-instance overrides.
SkeletalAnimationAsset— standalone reusable clips. Animation clips are no longer welded to the mesh they were imported with. Channels are keyed by bone name rather than bone index, so one clip binds to multiple rigs.SkeletalAnimationChannel { mBoneName, mSourceBoneIndex, mPositionKeys, mRotationKeys, mScaleKeys }. Surface:GetClipName,GetDuration,GetTicksPerSecond,GetDurationSeconds,GetChannels,GetEventTracks,GetSourceRigName,GetSourceBoneNames,GetSourceParentIndices,GetSourceBindPose,CopyFromEmbedded. New files:SkeletalAnimationAsset.cpp/h,SkeletalAnimationAsset_Lua.cpp/h.- Extract. Right-click any
SkeletalMeshstub → Extract Animations... opens a modal withPrefix(defaultSA_),Overwrite existing,Unique names, andExtract Selected/Extract All. Backed byActionManager::BeginExtractSkeletalAnimations. - Import. Asset browser →
Import Asset→ Import Animations reads animation-only.fbx/.glb/.gltf/.daefiles that carry no render mesh, oneSkeletalAnimationAssetper clip. Backed byActionManager::BeginImportAnimations. - Playback.
SkeletalMesh3Dgains an"Animation Assets"vector property.FindAnimationresolves embedded animations first, then themAnimationLookupMeshchain, then external assets, with lazy per-(asset, mesh)bone-index binding cached inmBoundExternalAnimsand invalidated byInvalidateAnimationBindings. - Lua (read-only):
GetClipName,GetDuration,GetDurationSeconds,GetTicksPerSecond,GetSourceRigName,GetNumChannels,GetChannelBoneName. There is no Lua API for the asset list itself; it is edited through the inspector.
- Extract. Right-click any
HumanoidAvatarAssetand bake-time retargeting.enum class HumanoidBone : uint8_twith 21 slots (Hips,Spine,Chest,Neck,Head,LeftShoulder,LeftUpperArm,LeftLowerArm,LeftHand,RightShoulder,RightUpperArm,RightLowerArm,RightHand,LeftUpperLeg,LeftLowerLeg,LeftFoot,LeftToes,RightUpperLeg,RightLowerLeg,RightFoot,RightToes) plusHumanoidBoneName. Asset surface:GetReferenceMesh,SetReferenceMesh,GetBoneName,SetBoneName,AutoMap,Validate,GetReferenceLocalBind. New files:HumanoidAvatarAsset.cpp/h. No Lua bindings — editor and bake-time only.- Auto-map.
NormalizeBoneNamestrips' ','_','-','.',':'and themixamorig,defandmchprefixes, covering Mixamo, Auto-Rig Pro, Rigify, and Blender.L/.Rsuffix conventions. - Retarget. Right-click a
SkeletalAnimationAsset→ Retarget....RetargetMode::NameRemap(UI:Tier 1 - Name remap (passthrough)) rewrites channel bone names only.RetargetMode::ReferencePose(UI:Tier 2 - Reference-pose aware) computesworldDelta = sourceWorldRot * inverse(sourceWorldBindRot),targetWorld = worldDelta * targetWorldBindRot,targetLocal = inverse(targetParentWorldRot) * targetWorld— bone-axis conventions drop out because everything is computed in a common frame. Hips translation is scaled by the bind-pose Y ratio. No IK end-effector solving, no twist-bone distribution, no synthesis for missing intermediate joints.
- Auto-map.
BoneMaskAssetand layered animation. Eight simultaneous animation slots perSkeletalMesh3D(NUM_ANIMATION_SLOTS = 8), each with independent weight, speed, bone mask, and layer mode.enum class AnimLayerMode { Replace, Additive, Count }.ASSET_VERSION_BONE_MASK = 37. New files:BoneMaskAsset.cpp/h,BoneMaskAsset_Lua.cpp/h,BoneMaskEditor.cpp/h(~1000 LOC).- Mask asset. Name-keyed include roots, exclude roots, and a self-only flag, resolved lazily against whatever mesh queries them with a one-entry cache. Surface:
GetTargetMesh,SetTargetMesh,GetIncludeRoots,GetExcludeRoots,GetSelfOnly,AddIncludeRoot,RemoveIncludeRoot,AddExcludeRoot,RemoveExcludeRoot,Resolve,InvalidateCache. Because masks are name-keyed, the same asset drives any rig sharing those bone names. - Node API.
PlayAnimationMasked,PlayAnimationAdditive,SetSlotMask,SetSlotLayerMode,SetSlotWeight,FadeSlotWeight.ActiveAnimationgainsmBoneMask,mWeightTarget,mWeightFadeRate,mLayerMode. - Blend math. Replace slots
glm::mixposition and scale andglm::slerprotation by slot weight. Additive slots accumulate(clip - bind) * weightfor position and scale, andslerp(identity, clipRot * inverse(bindRot), weight)for rotation. All of it runs on decomposed local TRS before the parent-chain walk, so every backend — Vulkan, GX, C3D, and addon targets — works with no per-platform code. - Supporting
SkeletalMeshadditions.GetBindPosePos,GetBindPoseRot,GetBindPoseScale(decomposed local-bind TRS cached inInitBindPose),GatherDescendants,GatherSubtreeBoneSet. - Lua. On
SkeletalMesh3D:PlayAnimationMasked,PlayAnimationAdditive,SetSlotMask,SetSlotWeight,SetSlotLayerMode,FadeSlotWeight. OnBoneMaskAsset:GetTargetMesh,SetTargetMesh,GetSelfOnly,SetSelfOnly,GetNumIncludeRoots,GetIncludeRoot,AddIncludeRoot,RemoveIncludeRoot,GetNumExcludeRoots,GetExcludeRoot,AddExcludeRoot,RemoveExcludeRoot. New global enum tableAnimLayerModewithReplaceandAdditive. - Bone Mask Editor. New dock under View > Bone Mask Editor, also reachable via right-click a
BoneMaskAsset→Edit Bone Mask. Owns its own previewWorld,Camera3D,DirectionalLight3D,SkeletalMesh3Dand offscreen color/depth targets, with orbit camera, grid toggle, animation selection, and play/loop/scrub. Included bones draw as green parent-to-child segments. PlayAnimationandQueueAnimationgained a new argument order —PlayAnimation(name, slot, loop, speed, weight)andQueueAnimation(name, dependentAnim, slot, loop, speed, weight). The legacy orders still work; dispatch is on whether argument 3 is an integer or a boolean. Source-compatible.
- Mask asset. Name-keyed include roots, exclude roots, and a self-only flag, resolved lazily against whatever mesh queries them with a one-entry cache. Surface:
- Procedural pose-modifier hook.
struct SkeletalLocalPoseTransform { mPosition, mRotation, mScale, mValid }andclass ISkeletalPoseModifierwithModifySkeletalPose(SkeletalMesh3D*, SkeletalMesh*, std::vector<SkeletalLocalPoseTransform>&, float), registered throughSkeletalMesh3D::RegisterPoseModifier/UnregisterPoseModifier. Modifiers run after blending, masking and additive layering, and before the local pose is composed into bone matrices — the intended consumers are jiggle-bone, spring, and spline-IK addons. - Skeletal introspection on the plugin C API. Five new
PolyphaseEngineAPIfunction pointers —SkeletalMesh_GetNumBones,SkeletalMesh_GetBoneName,SkeletalMesh_GetBoneParentIndex,SkeletalMesh_FindBoneIndex,Node_GetSkeletalMesh— so an addon can build bone-picker UI without linking engine asset types. A null or non-skeletal asset reads as a zero-bone skeleton; parent index-1is root. - New asset types force-linked in
Engine.cpp:FORCE_LINK_CALL(SkeletalAnimationAsset),FORCE_LINK_CALL(HumanoidAvatarAsset),FORCE_LINK_CALL(BoneMaskAsset). NewAssetReftypedefsSkeletalAnimationRef,HumanoidAvatarRef,BoneMaskRef. MeshSocketonSkeletalMesh:mName,mBoneName,mPosition(0,0,0),mRotation(euler degrees),mScale(1,1,1). Keyed by bone name (the same pattern asBoneMaskAsset) so a reimport that reorders bones does not relocate attached props. Asset API:GetNumSockets(),GetSocket(i),GetSocketMutable(i),GetSockets(),FindSocketIndex(name),AddSocket(socket)(empty name →"Socket"; collisions suffixed_1,_2, …),RemoveSocket(i),GetSocketLocalMatrix(i)(T·R·S).ASSET_VERSION_BONE_SOCKETS = 39,ASSET_VERSION_CURRENT = 39.SkeletalMesh::SaveStream/LoadStreamserialize the socket list (meshes below v39 load with zero sockets; resaving promotes them).SceneNodeDef::mParentBonewidenedint8_t → int32_t(Int32 at v39, Int8 below).Node3Dattachment by name. New serializedStringproperty"Attach Socket"(mAttachSocket). API:AttachToSocket(SkeletalMesh3D*, socketName, keepWorldTransform=false, childIndex=-1),SetAttachSocket(name),GetAttachSocket().mParentBoneIndex/mAttachSocketIndexare now a lazily-rebuilt cache (ResolveAttachSocket/InvalidateAttachSocket); sockets win over bones on a name collision, and missing targets log a warning. Lazy resolution is what makes scene load work:Scene::Instantiateapplies properties before adding the child, andAttach()clears the attachment, so an eagerly-resolved index would be destroyed.AttachToBonenow also records the bone name so bone attachments round-trip through scene save. The whole attachment funnels throughGetParentTransform()—parentWorld * boneMatrix * mInvOffsetMatrix * socketLocal— soUpdateTransform,SetWorldPosition,SetWorldScale, dirty propagation andUpdateAttachedChildreninherit socket support with no per-backend work.Scene::Instantiatetreats theAttach Socketproperty as authoritative; the legacymParentBoneindex is used only when no name is set.SkeletalMesh3D.GetBoneWorldMatrix(i)=node world * skinning matrix * mInvOffsetMatrixis now the single source of truth for three previously duplicated bone-space derivations;GetBonePosition/RotationQuat/Scaledecompose it. World-space socket queries:FindSocketIndex,GetNumSockets,GetSocketName(i),GetSocketTransform(name)(falls back to a bone of that name),GetSocketPosition,GetSocketRotationQuat,GetSocketRotationEuler.UpdateAttachedChildrenalso dirties children with a non-empty socket name so freshly loaded props do not sit at bind pose.- Animation time access.
GetAnimationTime(anim)(−1 if not active),GetAnimationNormalizedTime(anim)(0..1),SetAnimationTime(anim, seconds),SetAnimationNormalizedTime(anim, t),GetAnimationDurationSeconds(anim). - Animation notifies.
AnimationNotify { mAnimName, mNormalizedTime, ScriptFunc mCallback, mHandle };AddAnimationNotify(anim, normalizedTime, ScriptFunc) → handle(handles start at 1; −1 on invalid input),RemoveAnimationNotify(handle),ClearAnimationNotifies(anim = nullptr). Registered at runtime, unlikeEvent_*bones which need a DCC re-export and support one handler per node. Three deliberate differences from the legacyDetectTriggeredAnimEvents: detection runs outside theweight > 0guard so a slot still fading in still fires; the interval test is sign-agnostic ([min(prev,cur), max(prev,cur)]) so scrubbed playback withmSpeed = 0and reverse playback both work, and a frame spike that steps past the threshold is caught; loop wrap is detected as|curNorm - prevNorm| > 0.5and the covered interval is the two outer segments, so keys near t=0 are not dropped each loop. Dispatch is deferred to the end ofUpdateAnimationvia amPendingNotifiesmember (not a function-local static, becauseUpdateAnimationre-enters through the inherit-pose path and a callback can re-enter too); the list is swapped out before firing so a callback may freely mutate notifies ormActiveAnimations. One-timeLogWarningif notifies are registered underAnimationUpdateMode::OnlyUpdateWhenRendered, which never advances time for a culled mesh. - Editor. Properties panel for
SkeletalMeshgains aSockets (N)section with+add, per-socketName,Bonecombo,Position(0.01 step),Rotation(0.5),Scale(0.01),Xremove, and a red "Bone '%s' not found" warning. Sockets draw as RGB axes (length0.25) whenever aSkeletalMesh3Dusing that mesh is selected.Node3D::DrawCustomPropertyrendersAttach Socketas a combo ((None)/Sockets/Bones) routed throughActionManager::EXE_EditProperty(undoable, multi-select); hidden unless the parent is aSkeletalMesh3D. Scene panel:Attach Selected To Socketsubmenu (beforeAttach Selected To Bone) via newActionManager::AttachSelectedNodesToSocket();Create Socket From Selectionbakes a bone-attached node's local offset into a new socket, re-points the node withSetAttachSocketand zeroes its transform so the offset is not double-applied.ActionAttachNodetakes an optionalsocketNameand recordsmPrevSocketNameso undo restores either socket or bone attachment in one step. Reimporting a skeletal mesh carries sockets across (keyed by bone name, warning if the bone vanished) — they have no source-file counterpart and would otherwise be wiped. - Lua.
Node3D:AttachToSocket(newParent, socketName, keepWorldTransform?, childIndex?),Node3D:SetAttachSocket(string)(empty string detaches),Node3D:GetAttachSocket() → string;SkeletalMesh3D:GetNumSockets(),GetSocketName(index)(nil when out of range),GetSocketPosition(name) → Vector,GetSocketRotation(name) → Vector(euler degrees; both accept a bare bone name),GetAnimationTime(name),GetAnimationNormalizedTime(name),SetAnimationTime(name, seconds),SetAnimationNormalizedTime(name, t),AddAnimationNotify(name, t, fn) → integer,RemoveAnimationNotify(handle),ClearAnimationNotifies(name?). - Gotchas.
AttachToSocketreparents (aRemoveChild/AddChildround trip that reorders siblings); swapping sockets on the same parent must go throughSetAttachSocket.UpdateTransformstill overwrites scale withvec3(mScale.x)when the node has children (the anti-shear guard), so a socketed prop with a child effect loses its Y/Z scale.
Lighting — SpotLight3D
- New node
SpotLight3D : PointLight3D(Nodes/3D/SpotLight3d.h/.cpp, type name"SpotLight", default node name"Spot Light",FORCE_LINK_CALL(SpotLight3D)). Deriving fromPointLight3Dmeans radius, domain, culling and every existing point-light code path apply unchanged — and a backend that does not checkLightType::Spotrenders it as an omni point light rather than dropping it.- Properties (category
Light):Inner Angle(default30.0),Outer Angle(default45.0) — half-angles in degrees.SetInnerAngleclamps to[0, 89.9],SetOuterAngleto[0.1, 89.9].GetDirection()returns the node's forward vector;SetDirection(dir)callsLookAt(worldPos + dir, up=(0,1,0)). NewLight3D::IsSpotLight3D()virtual. Dispatch onIs()/As<>must testSpotLight3DbeforePointLight3D(done inRenderer::SetLightDataandRayTracer). LightDatagainsmInnerConeAngle/mOuterConeAngle.Renderer::SetLightDatafillsLightType::Spotwith outer clamped[0.1, 89.9]and inner clamped[0, outer]; point/directional/preview lights zero both.- Vulkan.
LightUniformDatapads becomemConeCosInner/mConeCosOuter(cos of radians; empty slots get1.0/1.0); GLSLLightDatainCommon.glslmirrors it.Forward.fragandForwardSpec.fraghandleLIGHT_TYPE_SPOTin the point branch, scaling attenuation byclamp((dot(dir, -L) - cosOuter) / max(cosInner - cosOuter, 0.0001), 0, 1). - Lightmap bake.
RayTraceLightType::Spot,RAY_TRACE_LIGHT_SPOT 2;RayTraceLightpads becomemConeCosInner/mConeCosOuter;RayTracer::UpdateRayTracingSceneemits spot lights with shadows, intensity and cone cosines;LightBakeDirect.compapplies the same cone factor. - GX (Wii/GameCube).
GX_InitLightDirwith a view-space direction andGX_InitLightSpot(cutoff, GX_SP_COS2), cutoff = outer angle clamped[1, 89]; other lights keepGX_SP_OFF. Inner angle is not used (single hardware cutoff). - C3D (3DS).
LightEnvgainsmLightSpotLuts[8],mLightSpotInner[8],mLightSpotOuter[8]. A spot LUT is built viaLightLut_FromFunc(SpotAttenFunc)and regenerated only when the angles change;C3D_LightSpotDir/C3D_LightSpotLut/C3D_LightSpotEnableper light. - PSP / PS2 / addon targets: no in-tree changes; they render a spot as a point light until their backends add cone handling.
- Editor.
BASIC_SPOT_LIGHT "Spot Light"in the Basic 3D spawn menu (defaults: white, radius10.0,LightingDomain::All);EditorAction::Spawn_SpotLightbound to Ctrl+Alt+2; hierarchy iconICON_LUCIDE_SPOTLIGHT(checked beforePointLight3D); proxy draw isSM_Sphereat 0.2 scale plus anSM_Conepointer, and when selected a preview cone with base radiusmRadius * tan(outer).ImportScenemapsaiLightSource_SPOTtoSpotLight3D(radius50.0, Assimp half-angle radians converted withglm::degreesintoSetOuterAngle/SetInnerAngle). - Lua (
SpotLight3D, inheritsPointLight3D):SetInnerAngle(number),GetInnerAngle(),SetOuterAngle(number),GetOuterAngle(),GetDirection() → Vector,SetDirection(Vector). NewLuaMeta/SpotLight3D.luaandDocumentation/Lua/Nodes/3D/SpotLight3D.md.
- Properties (category
- Console-aware preview lighting.
EditorState::ShouldPreviewConsoleLighting()returns true when the current build target id/platform (or, with no target, the Game Preview preset name via newGamePreview::GetSelectedPresetName()) matches a console token (gamecube,wii,3ds,psp,ps2,dreamcast, …). InRenderer::GatherLightDatathat clampscolor * intensitytomax(GetColorScale(), 1.0)with intensity reset to1.0and suppresses the synthetic preview directional light, so the desktop viewport matches fixed-function headroom on GX/C3D/PSP/PS2.View > Preview Lightingis disabled and relabelledPreview Lighting (off: console target).
Audio
- Streaming
SoundWaveplayback. NewStreamingbool property onSoundWave, plusIsStreaming,GetStreamPcmOffset,GetStreamPcmSize.ASSET_VERSION_SOUNDWAVE_STREAMING = 38. In a shipped build the PCM is not loaded resident — the.octrecords the payload's byte offset andAudioManagerre-opens the file and feeds the platform push-PCM stream voice in chunks.SaveStreamforcescompress = falsefor streaming waves, since there is no per-chunk Vorbis decode.- Editor asymmetry, by design.
SoundWave::LoadStreamonly takes the skip-resident-PCM path under#if !EDITOR. Packaging re-serializes the in-memorySoundWave, so a skipped load in the editor would writewaveDataSize=0and ship an.octwith no audio. ConsequentlyGetStreamPcmSize()is0in the editor andPlaySound2D/PlaySound3Dfall through to resident playback there; theStreamingflag only changes behavior in a built game. - Voice pool.
kMaxStreamingSources = 4, a pool separate from the static voice slots that never entersFindAvailableAudioSourceIndex.StreamingSourcecarries a retry stash, a prebuffer gate (kStreamPrebufferSec = 0.15f, the voice stays paused until 0.15 s is queued), loop-by-seek, and 8-to-16-bit widening. New internals:StartStreamingMusic,UpdateStreamingSources,CloseStreamingSlot,IsStreamingPlaying,StopStreamingBySound,StopAllStreaming,StreamingIOThread.AudioManager::InitializeandShutdownare no longer empty — they create and destroysStreamMutexand tear down live streams. - Streaming voices play non-spatially.
PlaySound3Don a streaming wave routes to the 2D streaming path. - Graceful degradation.
AUD_OpenStreamreturning 0 logsAudioManager: AUD_OpenStream failed for '%s' (streaming unavailable on this platform)and returns false rather than faulting. A wave reportingGetStreamPcmSize() == 0is refused up front so theAudio3Dauto-start pass cannot re-trigger in a loop. Backend status: XAudio2, Linux, Dolphin and 3DS DSP support stream voices; the 3DS CSND fallback and the Android backend do not.
- Editor asymmetry, by design.
- Seekable file API.
struct SysFileplusSYS_FileOpenRead,SYS_FileRead,SYS_FileSeek,SYS_FileClose, declared inSystem.hand implemented once inSystemUtils.cppso every platform links it without per-backend work. The handle transparently spans Content Pak entries (an independent handle at the entry offset, so long-lived seeking does not fightContentPak::Readfor the file position) and obfuscated containers (logical decoded cursor, per-chunkContentObfuscation::DecodeRange). - Platform-conditional chunk size.
kStreamChunkBytesis 128 KB underPOLYPHASE_PLATFORM_ADDONand 16 KB elsewhere. On slow-media consoles each blocking filesystem read carries a large fixed command latency, so reads are made big and rare — 128 KB is roughly one read per six seconds of 22 kHz audio. Desktop I/O is fast and keeps 16 KB. - Asset load-byte cap.
Asset::LoadFilenow passes a cap toStream::ReadFile; underPOLYPHASE_PLATFORM_ADDONaSoundWavegets 512 KB, dispatched offRuntimeName()rather than a new virtual so theAssetvtable ABI is undisturbed.SoundWave::LoadStreamdetects a truncated non-streaming payload and logsSoundWave::LoadStream: '%s' PCM (%u bytes) exceeds the load cap and is not marked streaming; sound disabled. Mark large audio as streaming. - Animation keyframe lookup is now binary search. New file-local
template<typename KeyT> FindKeySegment;FindScaleIndex,FindRotationIndexandFindPositionIndexare one-line wrappers. The old linear scan cost grew with distance into the clip, worst on the last frames before a loop, which surfaced as a per-loop hitch on slow targets.
Shipping and Content Protection
Static Content — obfuscated shipping assets. New
ContentObfuscation.cpp/h(341 LOC). Cooked.octassets,.luascripts andAssetRegistry.txtship wrapped in a 24-byte container (kHeaderSize = 24) carrying magicPLYOBF, version, flags, decoded size, a per-file salt, an FNV-1a-32 payload checksum and a header self-check. Enabled by theStatic Contentcheckbox in the build profile; serialized as"staticContent"inBuildProfiles.jsonand absent-means-false, so existing profiles are unchanged. This is obfuscation, not DRM — the key is a compile-time constant and therefore ships inside the executable. It defeats openingPlayer.luain a text editor and casual asset ripping; it does not defeat a disassembler.- Endian-independent. Every header field is read and written a byte at a time and the keystream byte is extracted by shifting, never by aliasing a
uint32_t, so big-endian GameCube and Wii decode identically to desktop with no#if. - Offset-addressable. A byte's keystream depends only on its own payload offset, so truncated reads, mid-file seeks, the console load cap and streaming audio all work.
- No per-file state and no allocation. Decode is in-place and single-pass, reentrant on the async asset-load worker, and does not double peak memory on a multi-megabyte console texture.
- Detection is by magic, not by config flag, and the header self-hash makes false positives effectively impossible — so plain and obfuscated files coexist in one package and the editor reads its own project tree untouched.
- Decode chokepoints.
Stream::ReadFilevia newStream::DecodeObfuscatedData;Asset::LoadEmbeddedfor the embedded byte-array table;AssetManager::DiscoverEmbeddedAssetsfor the asset header read at registration time;ScriptUtils::RunScriptfor embedded Lua; and theSysFilefamily for streaming audio. Missing theDiscoverEmbeddedAssetssite presents as Embedded-plus-Static crashing while Embedded-only and Static-only both work. - Static matters most alongside Embedded: without it an embedded build carries Lua source and every asset path as plaintext in the binary.
- Endian-independent. Every header field is read and written a byte at a time and the keystream byte is extracted by shifting, never by aliasing a
Content Pak — single-archive shipping. New
ContentPak.cpp/h(526 LOC). Folds assets, scripts, shaders, the.octpand the asset registry into oneContent.pak, then removes the loose copies, so filenames and the folder tree are hidden as well as file contents and no per-asset directory entry is spent on the target filesystem. Enabled by theContent Pakcheckbox, drawn as a child ofStatic Content; serialized as"contentPak".- Layout. A 32-byte plain header (
kPakMagic = "PLYPAK1\0",kPakVersion = 1, entry count, index offset and size, FNV-1a-32 header check), then per-entry obfuscated containers concatenated, then a 24-byte-per-record index (kRecordSize = 24: path hash, data offset, data size, path offset, path length) sorted by FNV-1a-64 path hash and itself obfuscated. Only the index is resident — roughly 10 to 20 KB for a few hundred entries — and entries are read and decoded on demand, so console memory behavior is unchanged. Lookup is a binary search on hash confirmed against the stored path, so a collision is a miss and never a wrong asset. Reads are mutex-guarded because the async asset loader shares the handle. - Keys are package-relative with forward slashes, for example
Game/Assets/Textures/T_Grass.octandEngine/Shaders/GLSL/bin/Forward.vert.Config.iniis never packed —ReadEngineConfigruns before the mount — and raw assets (.png,.json,.mp4,.rcss) stay loose because addon code opens those with its own file I/O. - Mount timing.
Engine::Initializeunder#if !EDITOR, beforeLoadProject, tryingContent.pak, then<projectDir>Content.pak, then<projectName>/Content.pak, and falling back toSYS_GetAbsolutePathfor device-prefixed paths such asromfs:/,host:and/cd/. Mounting forcesmUseAssetRegistryon and re-asserts it afterLoadProject, which would otherwise reset the flag when it re-reads the projectConfig.ini— the symptom of getting that wrong is a black screen withNo default scene found. - Shader discovery. Global Vulkan shaders were found by walking
Engine/Shaders/GLSL/bin/, which a packed build has no directory for. NewContentPak::List(prefix, outKeys);VulkanContext::CreateGlobalShadersenumerates the pak index and falls back to the directory walk when no pak is mounted or the pak carries no shaders. - Checkbox visibility. New
PlatformUsesShaderFiles(Platform)returns true only for Windows, Linux and Android; the pak checkbox hides when it would be redundant against Embedded, and a stale savedtrueis force-cleared so it cannot silently apply. Build-target addons opt out cosmetically viaPOLYPHASE_OPT_HIDE_CONTENT_PAK("polyphase.hideContentPak"="1") in the profile's target options, with no build-target API version bump. - Cook ordering is load-bearing.
ObfuscatePackagedContentandPackPackagedContentrun inBuildPhase1after the embedded generators (which read cooked files back throughStream::ReadFile, now decoding) and before the 3DS romfs copy (which clones the whole packaged directory). Pack prunes loose originals only after a successfulContentPak::Build, falling back to loose obfuscation on failure. - Build cache.
BuildCache::GetManifestPathnow takesstaticContentandcontentPak, producingBuildManifest_<Platform>_<Embedded|External>[_Static][_Pak].json, and both flags are written into the manifest JSON. Without this, flipping either checkbox reportedUpToDateand shipped the previous build.
- Layout. A 32-byte plain header (
Build All profiles. New
Build Allbutton besideBuildin the Packaging window, tooltipBuild every profile sequentially; stops on first failure.TickBuildAllQueueruns every frame fromPackagingWindow::Drawbefore the open-state early-out, so an in-flight queue survives closing and reopening the window. Force Rebuild is force-enabled and locked for the duration and restored afterwards, because the build-cache short-circuit stores a false success that the queue would read as a real failure.Cancel Allstops the queue but not the in-flight build.GetEngineContentDir(subDir)inEngine.h— resolves shipped engine content whether it sits atEngine/<sub>relative to the working directory or inside a per-project deploy folder. Used byEngine::Initialize,ScriptUtils::RunScript's engine-script fallback, and asset discovery. Returns the canonical root-relative form untouched when a pak is mounted, since the pak index is keyed on it.Linux RPM and AppImage build targets. Two new built-in targets registered in
BuiltInBuildTargets::RegisterAll:polyphase.linux.rpm("Linux (RPM)") andpolyphase.linux.appimage("Linux (AppImage)"), categoryDesktop,basePlatform = Platform::Linux,binaryExtension = ".elf"— declaring.rpmwould break the "<projectName><ext>exists" check inFinalizeLocalBuild.GetCompileCommandstays null so the legacyMakefile_Linux_Gamepath builds the ELF; onlyValidate,PostPackageandDrawProfileOptionsare set.- RPM (
RpmPackager): optionsrpm.version(1.0.0),rpm.release(1),rpm.license(Proprietary),rpm.summary,rpm.prefix(/opt),rpm.requires(vulkan-loader, libxcb, alsa-lib),rpm.iconPath,rpm.wslDistro. Generates a spec with%global debug_package %{nil},__os_install_post %{nil},_build_id_links none,AutoReqProv: no; a wrapper at/usr/bin/<pkg>thatcds into<prefix>/<pkg>before exec; hyphens in Version/Release become_. Scratch inIntermediate/rpm. - AppImage (
AppImagePackager): optionsappimage.version,.categories(Game;),.iconPath,.updateInformation(appimagetool -u),.runtimePath(offlinemksquashfs+catpath),.wslDistro.AppRunresolves$HEREviareadlink -fandcds intousr/bin;chmod 0755on AppRun and the ELF because the engine never sets exec bits; falls back toPolyphaseLogo_128.pngwhen no icon;APPIMAGE_EXTRACT_AND_RUN=1so FUSE is not required. Output<AppName>-x86_64.AppImage. LinuxHostShell(new):UsesWsl()(true on Windows),ToShellPath()(M:\x→/mnt/m/x),Quote(),BuildCommand()(wsl [-d distro] bash -lc "..."),Run(),RunScript()(writes an LF script so only the quoted path crosses the shell boundary),EnsureHostDir(),HasTool()(command -v),GetOption(),Report(),WriteTextFileLF()in binary mode — CRLF scripts die with "bad interpreter".- Dispatch rule change.
ActionManager::FinalizeLocalBuildnow callsPostPackagewhenever the callback is non-null, not only for!mIsBuiltIn.mIsBuiltInnow only decides platform ownership:IsCanonicalTargetForPlatform()(id ==BuiltInBuildTargets::IdForPlatform) →Packaged/<PlatformName>/; everything else →Packaged/<targetId>/viaResolvePackageSubdir. - Build-cache variant key.
ResolveBuildVariantKey()= sanitised target id + FNV-1a hash of the sorted profile-option map, threaded throughBuildCache::CheckRebuildNeeded / BuildCurrentManifest / LoadManifest / GetManifestPath(newtargetVariant,outputDirectoryparams; manifest gainstargetVariant). RPM, AppImage, PSP, Dreamcast and PS2 all share the Linux base platform and previously read oneBuildManifest_Linux_External.json, so switching targets falsely reported "up to date" andPostPackagenever ran. Canonical targets pass""so legacy manifest filenames are unchanged.mBuildState.mTargetOptionsis now snapshotted and restored across the async build like the other flags; controller-server and CLI callers otherwise hitPostPackagewith an empty map. PolyphaseBuildContext.WriteOutputLine/.Logwired viaBuildCtx_WriteOutputLine→ActionManager::AppendBuildOutput(made public) andBuildCtx_Log(severity →LogError/LogWarning/LogDebug).- Linux save-dir fallback (
System_Linux.cppGetSaveDir()):$POLYPHASE_SAVE_DIR→<projectDir>/Savesif writable →$XDG_DATA_HOME/<Project>/Saves(default~/.local/share). Needed because the AppImage mount and the/optprefix are read-only. NewEnsureDirPath()doesmkdir -p.
- RPM (
*Disc-image content layout (beta.18).* New target-option key
POLYPHASE_OPT_DISC_CONTENT="polyphase.discContent"(BuildProfile.h); a build-target addon that emits a disc image sets it to"1".PackagingWindow::DrawProfileSettingsthen forces Embedded off, Static Content on and Content Pak on, draws the three checkboxes insideBeginDisabled/EndDisabledwith the noteDisc layout set by the build target: Embedded off, Static Content and Content Pak on., andExecuteLocalBuildapplies the same forcing (buildEmbedded/buildStatic/buildPak) so a queued profile whose settings were never drawn this session still builds the only layout that boots. Previously nothing stopped a profile from shipping loose files onto an ISO.*Shader compile without the Vulkan SDK (beta.17).*
Engine/Shaders/GLSL/compile.batwas(( %VULKAN_SDK%/Bin/glslc.exe … ) && ( echo Compile Successful )) || pause; the editor streams the script into the build window, so any failure — including an unset%VULKAN_SDK%malforming every command — hitpauseand waited forever for a keypress with nothing on screen. It now usessetlocal EnableDelayedExpansion, resolvesGLSLCfrom"%VULKAN_SDK%\Bin\glslc.exe"then the firstwhere glslchit then a hard error naming the LunarG URL (exit /b 1), echoesUsing %GLSLC%, quotes every path, skips.glslcase-insensitively (if /I NOT "%%~xf" == ".glsl"), setsFAILED=1per file, and exits1/0.ActionManagergainedFindGlslcExecutable()($VULKAN_SDK/Bin/glslc.exeor/bin/glslc, thenwhere glslc/command -v glslcviaSYS_Exec,""when absent) andHasPrecompiledShaders(shaderDir)(everysrc/*other than.glslincludes has abin/counterpart, and at least one source exists).BuildPhase1clearsmBuildState.mShaderCompileCommandand logsglslc (Vulkan SDK) not found; skipping shader compilation and packaging the precompiled shaders in Engine/Shaders/GLSL/bin.when glslc is missing andbinis complete, and warns (naminghttps://vulkan.lunarg.com/sdk/homeandVULKAN_SDK) then proceeds when neither exists;BuildCompileThreadFuncdowngrades a non-zero shader exit to a warning (Continuing with the existing precompiled shaders... they may be stale if shader sources were edited.) and forcesshaderExit = 0whenbinis complete and the build was not cancelled — the same escape hatch keying past the oldpausegave. The Windows shader command is nowcd /d "<dir>" && "./compile.bat": without/d,cddoes not change drive, so an engine onM:with the shell onC:silently never found the script.*Windows SDK release (beta.17).*
Installers/package_windows_sdk.pyusedos.environ.get("VULKAN_SDK", "")in the Vulkan-header staging step without importingos; every Windows SDK-zip release run crashed withNameError: name 'os' is not definedafter the earlier staging steps had already run.import osadded.
Profiling
- Memory Snapshot Profiler (
Engine/Source/Editor/MemorySnapshot/, new).Tools > Memory SnapshotopensMemorySnapshotWindow(Open / Close / Draw / CaptureNow / Shutdown, accessorGetMemorySnapshotWindow()). It captures the running game — the Game or 3DS preview surface, or live play-in-editor — never the editor viewport. Memory is attributed only to assets referenced by game-world nodes, so gizmos, editor fonts and thumbnails are excluded by construction.- Data model (
MemorySnapshot.h):SnapshotCategory { Geometry, Textures, Audio, Scripts, Materials, Fonts, Particles, Animation, Nodes, Other, FrameBuffer };SnapshotEntry { mName, mTypeName, mCategory, mCpuBytes, mGpuBytes, mRefCount, mDetail };MemorySnapshotadds whole-processmSystemRamUsed / mSystemVramUsed / mSystemTotalRam,mWasPlayingInEditor,mActiveAudioVoices, and an optional RGBA8 thumbnail (max 256 px, nearest-neighbour downscale ofGamePreview::CaptureScreenshotToMemory). - Sizing (
MemorySnapshotProfiler.cpp):GatherGameReferencedAssets()walks node and script properties (IsAssetDatumType) and expands transitively — mesh → material → texture, includingMaterialLiteslots read directly so the white fallback is not counted; font → atlas; particle → material.ClassifyAndSize()is format-accurate (BytesPerPixelperPixelFormat, CMPR = 0.5, ×1.3333 for mips), streaming-aware (SoundWave::IsStreaming()→ 0 resident bytes), and splitsSkeletalMeshinto Geometry plus Animation (keyframe bytes).BuildGameMemoryEntries()adds Lua source sizes from<project>/Scripts/and aFrame Buffersrow (w*h*4*2 + w*h*2, default 640x480 or the Game Preview size). - Serialization. Loose files in
<project>/Debug/DebugSnapshot_YYYY-MM-DD_HH-MM-SS.oct— a sibling ofAssets/, so the AssetManager never indexes them — withkSnapshotMagic = 0x504e5350("PSNP") andkSnapshotVersion = 1. Toolbar:Capture Snapshot,Save,Load...,Reveal Debug Folder. A RAM-budget combo carries 17 console presets from 2 MB to 32 GB (default index 6 = 64 MB) plusCustom, with a green / amber / red bar at ≤75% / ≤100% / over. The footprint metric ismGpuBytesif non-zero elsemCpuBytes, which avoids double-counting a texture's editor-only CPU copy. Sortable seven-column table. - Profiling window unified.
ProfilingWindow::EstimateMemoryForPlatformnow bucketsBuildGameMemoryEntries()output as the single source of truth, throttled to one recompute per 0.5 s viamCachedMemory. NewOtherbar;RT/FBrenamedFrame Buf. Without a build target it shows the game estimate (no budget cap) instead of raw process memory, and gainsCapture Snapshot/Open Memory Snapshot Windowbuttons plus a whole-process RAM/VRAM footnote.
- Data model (
Widgets and UI
- Fill anchors. Three new
AnchorModevalues —Fill,FillHorizontal,FillVertical— appended beforeCount, which moves 16 to 19. Inspector labelsFill/Fill Horizontal/Fill Vertical; LuaAnchorMode.Fill/AnchorMode.FillHorizontal/AnchorMode.FillVertical; XMLanchor="fill" | "fill-horizontal" | "fill-vertical". On a filled axisGetX/GetYreturn0andGetWidth/GetHeightreturn the parent extent, andmOffsetandmSizeare ignored. New predicatesAnchorFillsX,AnchorFillsY,FillsX,FillsY. Regenerated:Engine/Generated/LuaMeta/AnchorMode.lua,Engine/Generated/XML/PolyphaseUIDocument.xsd,Tools/generate_ui_xsd.py.SetAnchorModehandles transitions explicitly: entering a Fill mode zeroesmOffsetandmSizeon the newly filled axis so aFullStretchtoFillHorizontalswitch cannot leave a stale ratio-to-pixel conversion; leaving one restoresmSizeto1.0for the parent extent so the widget does not become zero-size and invisible.
Widget::SetParentRectOverride/ClearParentRectOverride/HasParentRectOverride, andWidget::UpdateRectis nowvirtual. When set,UpdateRecttreats the override as the effective parent rect; it is cleared automatically inSetParent. This is the slot mechanismArrayWidgethands to children.ArrayWidgetpadding and flex-grow. Four new float properties —Padding Left,Padding Top,Padding Right,Padding Bottom— plus an editor-onlyRefresh Layoutbutton property that marks self and every child dirty, lays out immediately and callsRenderer::DirtyAllWidgets.Tick,EditorTickandTickCommonwere removed in favor ofUpdateRectandPreRenderoverrides: adding, removing or reordering a child only marks the child dirty, never theArrayWidget, so a dirty-gated layout missed those cases.LayoutChildrenis a two-pass flex algorithm — pass one sums the array-axis size of non-Fill children and counts Fill children, thenfillSlot = (axisTotal - padBefore - padAfter - nonFillTotal - totalSpacing) / fillCountclamped at zero, then pass two stamps a slot rect on each Fill child. Non-Fill children get anchor-ratio compensation so aRightStretchorMidRightchild lands in its slot without manual negative offsets. OnlyFill*modes flex-grow;Stretch*children take the classic path.
ScrollContainer::SetContentWidget(Widget*), null restoring the legacy first-non-transient-child scan.ListViewWidget's internalArrayWidgetis transient, so the fallback scan skipped it and the container computed a zero content size — scrolling never activated.ListViewWidgetsizing. NewUpdateContentSize(sums item extents plus spacing, floors the cross extent at the viewport size, resizes the internal array widget and marks the scroll container dirty) called fromPreRender,AddItem,RemoveItem,UpdateItem,Clear,SetSpacing,SetOrientation,SetItemWidth,SetItemHeightandRebuildItems; andApplyItemSizeToAll, which indexes by size rather than range-for so a re-entrant mutation cannot dangle the end iterator.EnsureContainersnow wiresSetContentWidget(mArrayWidget).- Behavior change:
CreateItemgives the item wrapperAnchorMode::FillHorizontalon a vertical list (orFillVerticalon a horizontal one) when the cross-axis size is not fixed, so rows span the list by default. Setting a fixed cross-axis size keeps the previousTopLeftdefault.ApplyItemSizeandListViewItemWidget::SetContentWidgetnow skip auto-sizing on any axis that fills or stretches.
- Behavior change:
- Game View resolution drives widget layout. New editor-only
Widget::ComputeGamePreviewLetterbox, cached per frame on the frame number, computing a letterboxed sub-region of the editor viewport at the Game Preview target aspect. Root widgets use it as their parent rect and multiplymAbsoluteScaleby the letterbox scale, so fixed-pixel content scales proportionally down the tree.GetParentWidth/GetParentHeightreturn the authored target dimensions rather than scaled letterbox pixels. NewGamePreview::GetSelectedPresetWidth/GetSelectedPresetHeightread the selected preset rather than live render targets, so the letterbox stays correct with the panel closed. OutsideEditorMode::Scene2Dthe root scissor clips to the letterbox; in Scene2D it deliberately does not, so overshooting widgets stay visible for editing. - Game view border. New
DrawGameViewBorderoutlines the Viewport2D wrapper rect in Scene2D viaImGui::GetBackgroundDrawList(), clamped to the viewport so it cannot spill into adjacent panels, drawn in cyan with a one-pixel black shadow so it reads on any background and stays distinct from the orange Canvas and green selection outlines. - Game Preview
Project Settingspreset.GetAllPresetsinserts a synthetic top preset from AppSettings'WindowWidth,WindowHeightandFullscreen— the resolution the packaged game actually runs at — formatted* Project Settings %dx%dor* Project Settings %dx%d (Fullscreen). NewApplyProjectDefaults, called fromActionManager::OpenProject, resets the selection and force-recreates render targets, fixing a stale 640x480 target lazily created at editor startup before the project was known.BeginPlayInEditornow honorsmFullscreenviaSYS_SetFullscreen.
Editor and UX
Create Assetis categorized. Eight submenus replace the flat list:Scene;Material(Material (Lite),Material (Base),Material (Instance),Skybox Material,Prototype Grid Material);Animation(Timeline,Transform Animation,Sprite Animation,Humanoid Avatar,Bone Mask);Tilemap(Tile Set,Tile Map);VFX(Particle System);UI(UI Document,CSS Stylesheet);Input(Input Prompt Map,Input Prompt Style);Logic(Node Graph). Addon asset factories that opted into a bucket viaPolyphaseEngineAPI::SetAssetCategoryare appended under the matching built-in category.Othernode categories.sNodeOtherNamesbecamesNodeOtherCategorizedplussNodeOtherAddonGrouped, matching the existing 3D and Widget shape. NewClassifyOtherroutesPlayerInputRegistrartoInput;TimelinePlayer,SpriteAnimator,TransformAnimationNode3DandTransformAnimationWidgettoAnimation;NodeGraphPlayertoLogic; everything else toGeneric. Draw order isInput,Animation,Logic,Generic. The 3D and Widget menus each gained anAnimationbucket, andTransformAnimationNode3D/TransformAnimationWidgetare cross-listed there — they derive fromNode, so without it they were invisible in the menus users actually look in. Uncategorized addon nodes route toOther / Addons / <addonId>.- Add Node quick-spawn toolbar. Two rows of icon buttons in the Scene panel. Row one:
Add Node3D,Add Static Mesh,Add Skeletal Mesh,Add Instanced Mesh,Add Camera 3D,Add Audio 3D,Add Directional Light,Add Point Light,Add Particle 3D. Row two:Add Canvas,Add Quad,Add Button,Add Text,Add Input Field,Add Array Widget,Add Window,Add Timeline Player,Add Spline. Spawn parent is the single selected non-scene-linked node, else the world root. A separate+button (Add Node...) opens the full add-node popup beside the search filter, which now stretches to fill. - Assets panel quick-create buttons. A
+button (Create Asset...) opening the full hierarchy, plus direct-create buttonsCreate Scene(SC_Scene),Create Material (Lite)(M_Material),Create Tile Map(TM_TileMap),Create Timeline(TL_Timeline) andCreate Transform Animation(TA_TransformAnim). The cluster dims and no-ops with no project, no current directory, or an engine/addon directory selected. - Main menu bar shortcuts. Five right-aligned icon buttons left of the existing Hammer button:
Preferences,App Settings,Player Input Panel,Addons Manager,Build Profiles. - Script Creator
Node Reactions:section. A class combo overButton,CheckBox,Slider,SpinBox,ComboBox,ProgressBar,InputField,ScrollContainer,ListViewWidget,ListViewItemWidget,Window,DialogWindow,TimelinePlayerandAudio3D, each exposing its callbacks as checkboxes that emit empty stubs in declared order. Changing the class clears stale picks.- Same pass cleaned up generated scripts: a new
Lifecycle:section withSingleton(emits<Class>.Instance, aStart()that destroys duplicates and claims the instance, and aDestroy()that nulls it) andPersist across scenes (DontDestroyOnLoad)(emitsself:SetPersistent(true)); property defaults now emitif self.X == nil then self.X = ... endso inspector-set values survive; and vector constructors were corrected toVector.Create(...).
- Same pass cleaned up generated scripts: a new
- Script Panel right-click menu.
Open in External Editor(disabled unless a Lua editor is configured),Open in Script Editor, thenRename...,Duplicate...andDelete(hidden for engine scripts, which are read-only), thenReveal in Explorer. Right-click now also selects the row so the highlight matches the target. Operations are deferred until after the tree finishes drawing, because the walk holds pointers into a cached vector that mutating would invalidate. Rename validates against Lua identifier rules, rejects collisions, and rewrites the class name inside the file with a boundary-aware replace so the class table matches the new filename. - Mesh import brings materials and textures with it.
MeshImportOptionsexpanded tomMode,mPlaceInSubdirectory,mImportMaterials,mImportTextures,mPrefix,mDefaultShadingModel,mDefaultVertexColorMode, surfaced in the Import Mesh Asset modal asPlace in subdirectory,Prefix,Import Materials,Import Textures,Shading ModelandVertex Color Mode.ImportAssetandImportAssetCombinedtake the options through, and they ride the name-clash flow viaPendingImportClashso a re-driven import after a rename keeps the user's settings.- New shared helpers
MaterialImportContext,ImportOrFetchMaterialTextureandImportSceneMaterialsfactor the logic out of the scene-import path so mesh-asset import and scene import share it. Per material: glTF metadata mapsAI_MATKEY_GLTF_ALPHAMODEMASKtoBlendMode::MaskedandBLENDtoTranslucent,AI_MATKEY_GLTF_ALPHACUTOFFtoSetMaskCutoff, andAI_MATKEY_TWOSIDEDtoCullMode::None; up to four diffuse slots resolve to textures. Embedded GLB textures are extracted to a temp file, imported, and removed. Textures are namedT_<meshStem>_<prefix><name>and materialsM_<meshStem>_<prefix><aiName>, and the source material index is auto-assigned to the mesh — before the existing "user has a Material selected" override, so a manual pick still wins.
- New shared helpers
ActionReplaceNodeTypeand theReplace Withmenu. New undoableActionManager::EXE_ReplaceNodeType(Node*, TypeId)morphs a node's type in place: preserves the slot in the parent, transfers every property matched by name and type, moves all children, and preserves the persistent UUID so Timeline bindings and node-graph references stay resolvable. Surfaced for widgets asReplace With > Widget > <bucket> > <Type>, reusing the same category maps as the Add Node menu. Blocked for scene-linked nodes, sub-scene children and the root.PlayerInputRegistrarnode. NewNodesubclass with a singleInput Actionsproperty filtered toInputActionsAsset. OnStart()it unregisters every existing action and replays register, trigger and binding from the asset, mirroringPlayerInput.LoadActions. Categorized underOther > Input. New files:PlayerInputRegistrar.cpp/h.System.ListDirectory(path)inSystem_Lua, returning a 1-indexed array of tables withname(string) andisDirectory(boolean). Non-recursive; an unopenable path yields an empty table rather than an error.DirEntryis a LuaLS documentation class describing that table shape, not a bound userdata type. Generated stub added toEngine/Generated/LuaMeta/System.lua.Copy Hierarchynow captures everything.BuildHierarchyJsongainedAppendJsonEscapedString,AppendDatumJsonValueandAppendPropertiesJson, and emits full inspector state fromGatherProperties— transform, widget size, anchor, color, opacity, pivot, texture, material — so addon node types are covered automatically, plus ascriptPropertiesobject for script-exposed fields deduplicated against the main block. Datum type coverage mirrorsControllerServerRoutes.cppso copied output matches the controller REST API.typenow usesGetClassName()rather thanGetTypeName().ScriptUtils::ReloadAllScriptFilessweeps disk. Previously it only re-ran already-loaded scripts. It now walksEngine/Scripts/,<project>/Scripts/andPackages/<addon>/Scripts/viaAssetManager::GatherScriptFiles, so brand-new files and scripts pulled in through Luarequire()— which bypassesLoadScriptFile— are covered without a restart. CompanionInvalidateScriptPickerCacheforces the inspector's script picker to re-walk disk past its two-second debounce.AssetManager::UnregisterTransientAsset(Asset*), symmetric toRegisterTransientAsset. Removes the entry and clears the transient flag without destroying the asset; the caller keeps ownership. Added for addons that own aTexture*themselves but need it visible toIsAssetLivesoQuad::ResolveQuadTexturedoes not clear it as dangling on the firstPreRender.EditorImageCache(new, Vulkan-only).Get(absPath)→ImTextureIDwith negative caching,GetSize,Invalidate,InvalidateAll,RetirePending(drained at the top ofEditorImguiDrawbeforeNewFrame, with aDeviceWaitIdle),Shutdown(fromEditorImguiPreShutdown). Release is deferred becauseImGui_ImplVulkan_RemoveTextureis immediate andAddonsWindow::Closeruns mid-Draw.AddonsWindownow only memoizes paths inmThumbnailPathsinstead of owning Vulkan textures.- Addon registry reads an auto-generated
manifest.json.AddonManager::FetchManifest()reads one manifest per repository (triesmainthenmaster) withname,categories[] { id, name, description }andpackages[](skipsstatus != "published", dedupes by id).ParseAddonObjectmaps theengineblock (target,apiVersion,entrySymbol,binaryName,resolveMode),dependenciesandbuildTargets. Falls back to the legacy per-packagepackage.jsoncrawl. NewAddonCategorystruct,ContentMetadata::mCategory,GetCategories(). The Browse tab gets aCategory:combo (default "All Categories") and groups cards under collapsing headers in registry order with "Other" last. - Scene import "Share Materials/Textures".
SceneImportOptions::mShareAssetswithmSharedAssetDir = "_Shared":M_/T_assets drop the per-scene stem, land in a shared sibling folder, and existing ones are reused rather than re-authored and re-saved. glTF extras now honourpolyphase_material/material_path/octave_material,polyphase_collision/collision, andpolyphase_hidden/hidden(PolyphaseNodeExtras::mMaterialPath / mCollision / mHidden, -1 = unset).ActionManager::ImportScenenow returns theAsset*. - Controller server.
POST /api/assets/importacceptsscene: trueplussceneName,import{Meshes,Materials,Textures,Lights,Cameras},enableCollision,applyGltfExtras,shareAssetsandsharedFolder, and restores the asset directory afterwards so batch imports land as siblings."Root"is now an alias for the world root in node lookups; spawning with a missing parent returnsParent node not found. - Game Preview camera combo gains
(Active Camera)(sentinelmSelectedCameraIndex = -1, now the default) — the preview follows whichever camera is active instead of a pinned index. - Scripts panel reveal.
EditorState::BrowseToScriptPath()+mRevealScriptPath; requests are retried for 1 s, force theLua Scriptstab, clear the search filter, and compare on the.lua-stripped stem. The Create Script and Create C++ dialogs close on success. - Asset browser. Clicking a folder row selects it (
SetAssetDirectory,ImGuiTreeNodeFlags_Selected); newData Assetentry (DA_DataAsset) in the New Asset > Logic menu;FORCE_LINK_CALL(DataAsset). Node::OnSpawnedInEditor()— called only fromActionSpawnNodes::Execute, never on reload or clone, for one-time authoring setup such as a spline's starter point.- Multi-selection actions across panels. New
GatherHierarchyActionNodes(drops descendants of other selected nodes) andGatherAssetActionStubshelpers. Hierarchy: group drag-and-drop (payload stays a singleNode*; drop targets re-derive the set),Move > Top/Up/Down/BottomandHier_ReorderUp/Downhotkeys act on sibling blocks,DeleteusesEXE_DeleteNodes, right-click pulls the row into the selection, and single-select on an already-multi-selected row is deferred to mouse-up (sPendingSelectNode) so a group drag can start. Asset browser: multi-drag to folders,Save,Delete,Duplicate(auto-named when more than one). Scripts panel: Ctrl/Shift multi-select keyed by full path (display names collide across origins),Delete Selected,Copy Paths, multi-open. ActionManager::LaunchDesktopBuild()(used byBuildData,FinalizeLocalBuildandPackagingWindow::FinalizeBuild) launches desktop builds directly,cd-ing into the packaged directory first because the game resolvesConfig.iniand assets relative to cwd; returns false with "Cannot run a %s build on this host" when the target does not match the host.- Build Dependency window adds
devkitPPC/devkitARMtoolchain checks — a devkitPro install withoutwii-dev/3ds-devpassedCheckDevkitProbut failed mid-make with a missing-compiler error. CreateDirectoryRecursivepromoted from aNativeAddonManagerstatic intoEditorUtils.{h,cpp}.
Scripting Runtime
require()in packaged builds.EngineScriptSearcheris appended topackage.searchers(after the stock ones, so loose.luaon disk still wins in the editor) and resolves via newScriptUtils::ReadScriptSource(fileName, outSource): embedded table →<project>/Scripts/→ engineScripts/(Content.pak-aware). Dotted module names fall through to Lua's own searchers. Embedded-script deobfuscation is factored intoDecodeEmbeddedScript.- Reload-excluded boot scripts.
ScriptUtils::IsReloadExcludedScript(fileName)—EngineStartup,Startup,StartLuaPanda,LuaPanda,DataAssetTemplate— honored by both per-file hot reload andReloadAllScriptFiles.LoadScriptFile(fileName, className, reportMissingClass=true): disk-swept module scripts with no class table now log at debug level instead of error. DatumType.Functionexposed to Lua — an editor-only inspector button that calls the same-named method. ScriptGatherPropertiesentries accept atooltipfield →Property::mTooltip(editor-only, now copied in the copy ctor/assignment); property buttons show it on hover.ThirdPersonController.lua: animation names are now inspector propertiesanimationFall/animationJump/animationRun/animationIdle(defaults"Fall","Jump","Run","Idle"), so a rig with different clip names no longer needs a script copy.- Target screens from Lua.
Node:SetTargetScreen(int),Node:GetTargetScreen()(routes throughGetEffectiveTargetScreen, so it returns the resolved screen rather than the raw property),Node:GetScreenResolution() → Vector; global enumTargetScreen = { Top = 0, Bottom = 1, All = 255 }.Renderer.GetScreenResolution(index)now requires the index. - Bounds from Lua.
Node3D:GetAABB(),Node3D:GetHierarchyAABB(includeSelf = true),Primitive3D:GetAABB() / GetLocalAABB(),StaticMesh:GetBounds() / GetAABB(),SkeletalMesh:GetBounds() / GetAABB(). AABB tables carrymin / max / center / extents / size;Primitive3D:GetBounds()addsmin / max. World:GetLoadedSceneName() → string,World:GetLoadedSceneNames() → array.Spline3D:GetSplineLength(),GetClosestDistanceAlong(Vector),GetNumSplinePoints(),GetSplinePointPosition(i),GetSplinePointWorldPosition(i)(1-indexed).
Engine and Rendering
AABBstruct inEngineTypes.h, kept separate fromBoundssoDrawDatastays small: default ±0.5 box,MakeInvalid / MakeFromCenterExtents / MakeLarge,IsValid / IsLarge,GetCenter / GetExtents / GetSize / GetRadius / GetVolume,Contains,Intersects(box, sphere),Encapsulate,Expand,Transform(mat4)(nine-multiply-add refit),ToBounds.Node3D::GetAABB()is virtual (degenerate box at world origin by default) andGetHierarchyAABB(includeSelf = true)unions the subtree.Primitive3D::GetAABB()=GetLocalAABB().Transform(mTransform); the defaultGetLocalAABBcircumscribes the bounding sphere so subclass overrides are optional. Tight overrides:Box3D,Sphere3D,Capsule3D(Y-aligned, +radius caps),StaticMesh3D/SkeletalMesh3D(assetGetAABB(), withmBoundsScale/mBoundsRadiusOverridemirrored),InstancedMesh3D(union of transformed instances),Voxel3D.StaticMesh::mAABBandSkeletalMesh::mAABBare computed inComputeBounds()and deliberately not serialized.BoundsDebugModegainsBoxAllandBoxSelected(SM_Cubedebug draw, scale = half-size). Viewport focus (F) now framesGetHierarchyAABB().3DS dual-screen resolution order (
Engine.cpp,Renderer.cpp,World.cpp). If world 1 has no root, world 0 is rendered again for screen 1 withmTargetScreenFilter = screenIndex(SUPPORTS_SECOND_SCREEN && !EDITOR);World::FindCameraForScreen()(subtree membership, prefers the main camera) is installed as the camera override for that pass.Node::GetEffectiveTargetScreen(sceneRoot)resolves up to the scene-root-level child;Renderer::PassesTargetScreenFilter()applies the filter to lights too (0xFF= all).World::GetLoadedSceneName()/GetLoadedSceneNames()(mLoadedSceneName,mQueuedSceneName).Spline3DgainsGetNumSplinePoints,GetSplinePointPosition / GetSplinePointWorldPosition,SetSplinePointPosition,AddSplinePoint,ClearSplinePoints,GetSplineLength,GetClosestDistanceAlong(16 samples per segment, Catmull-Rom whenmSmoothCurveand ≥4 points),SetCloseLoop / IsCloseLoop,SetSmoothCurve / IsSmoothCurve. Exported for addons.AssetManager threadless mode.
ProcessNextBeginLoad()is shared by the load thread and a synchronous drain inUpdate()(max 4 loads per frame) whenSYS_CreateThreadreturns null, which is the case on single-threaded wasm.InputConstants.hconsole fallbacks are now#ifndef-guarded;PLATFORM_WEBis excluded fromHttpBackend_Stub/HttpClient.C3D.
GFX_CreateTextureResourceretriesTex3DS_TextureImporton the linear heap when VRAM is full (with a warning) and names failures;mColorScaleEnumis reset toGPU_TEVSCALE_1each frame;SYS_AlignedFreefixes a malloc/free mismatch.MaterialBase::GetLiteFallbackandMaterialLite::Createwarn once when falling back to white.*Fallback camera (beta.18).*
World::EnsureFallbackCamera()(new memberCamera3D* mFallbackCamera, accessorGetFallbackCamera()), called once perWorld::Updateafter the queued-scene swap so a scene loaded this frame is evaluated with its own cameras registered. Returns immediately whenIsHeadless()(cooks must not gain a node). WhenmRootNode != nullptrandGetActiveCamera() == nullptr(deliberatelyGetActiveCamera(), notmActiveCamera, so the editor camera andmCameraOverridedo not trigger it) it callsSpawnDefaultCamera(), names itFallback Camera(default settings at(0, 0, 10)) and logs once:World: scene '<name>' has no Camera3D -- using a transient fallback camera. Add a Camera3D to the scene; UI-only scenes need one too.RegisterNodenow also yields the active slot whenmActiveCamera == mFallbackCamera, so a real camera arriving later (scene load, spawned prefab, streamed sector) takes over; retirement (retired->Detach()) is deferred to the nextEnsureFallbackCamera()because detaching mid-RegisterNodeis unsafe;UnregisterNodenullsmFallbackCameraas it already didmActiveCamera. Root cause of the symptom: withmActiveCamera == nullptr,Renderer::Renderskipped the scene pass andGFX_RenderPostProcessPasses()— the only full-windowGFX_SetViewport/GFX_SetScissorpaint — and ran a bareRenderPassId::Clearover the scene viewport, so UI-only scenes drew offset into a corner with black bands (reproducible on Android; desktop often masked it).*UI pass scissor (beta.18).*
Renderer::Rendernow callsGFX_SetScissor(viewportX, viewportY, viewportWidth, viewportHeight)alongside the viewport set, immediately beforeGFX_BeginRenderPass(RenderPassId::Ui). It previously inherited the scene scissor (window viewport × Resolution Scale, fromGetSceneViewport), so with Resolution Scale below 1.0 the whole UI was clipped into that rect until some drawable widget set its own inWidget::Render; non-drawable widgets (Canvas, plainWidget) never do.*CSS
overflow(beta.18).*ApplyCSSProperty(UITypes.cpp) handleshiddenorclip→EnableScissor(true)andvisible→EnableScissor(false). Previously onlyhiddenwas handled, so a stylesheet could never override the scissor default thatButton,Canvas,InputField,ScrollContainer,SliderandWindowset inCreate(). (The FAQ and skill prose from the same commit still calloverflow: visiblea no-op; the code is authoritative.)*Vulkan
Imagelayout on mobile GPUs (beta.18).*Image::mLayoutandciImage.initialLayoutdefaultVK_IMAGE_LAYOUT_PREINITIALIZED→VK_IMAGE_LAYOUT_UNDEFINED: every image the class creates is OPTIMAL-tiled, DEVICE_LOCAL and never host-written, andPREINITIALIZEDis only meaningful for LINEAR-tiled host-written memory. The old path issued a barrier withVK_ACCESS_HOST_WRITE_BITunder a stage mask containing no HOST stage — ill-formed, forgiven by desktop drivers, treated as undefined by Adreno / Mali (texture-compression metadata), sampling black on the first upload.TransitionImageLayout's twoPREINITIALIZED → TRANSFER_SRC/DST_OPTIMALbranches usesrcAccessMask = 0like theUNDEFINEDbranches.Image::UpdaterestoredsavedLayoutunless it was exactlyPREINITIALIZEDandImage::Clearunless exactlyUNDEFINED; each missed the other sentinel, and both are illegal as a barriernewLayout; a sharedsavedIsSentinel/originalIsSentineltest now covers both and lands onSHADER_READ_ONLY_OPTIMAL. New signaturevoid Image::Update(const void* srcData, bool waitForCompletion = false): the staging copy and transitions are three fire-and-forget one-time command buffers with no fence — fine for a per-frame stream (frame 2 self-heals), wrong for a texture uploaded once and sampled immediately;truecallsvkQueueWaitIdle(GetVulkanContext()->GetGraphicsQueue()), andCreateTextureResource()(theTexture::Create()initial upload) passestrue. Named use case: a webcam snapshot bound straight to aQuad.Texture::SetRetainPixels(bool)/GetRetainPixels()(beta.17) (bool mRetainPixels = false, placed in the existing 3 bytes of padding beforemUvMaxsosizeof(Texture)and every member offset are unchanged and already-built addon DLLs stay ABI-compatible). Non-editorTexture::Create()unconditionally didmPixels.clear(); shrink_to_fit()after the GPU upload, soGetPixels()was empty in a packaged build while the identical code worked in the editor. The clear is skipped when set, andUpdatePixels()keeps the CPU copy mirrored while it is on (restructured around akeepCpuCopyflag, always true underEDITOR). For runtime-generated textures read back on the CPU (webcam snapshot PNG-encoded and uploaded, procedural image saved); costswidth * height * 4, so it stays off for draw-only feeds.StaticMesh::Import(beta.18) addsaiProcess_GenSmoothNormalsto theReadFileflags (alongsideaiProcess_FlipUVs | aiProcess_JoinIdenticalVertices | aiProcess_Triangulate) and null-checksmesh->mNormalsat bothvertices[i].mNormalassignment sites inStaticMesh::Create(fallbackglm::vec3(0, 0, 1)), matching the existing texcoord guards. A bare Wavefront.objwith novnlines — hit with an AI-generated mesh — leftmNormalsnull and segfaulted.POLYPHASE_THREAD_LOCAL(beta.18) (Engine/Source/System/SystemConstants.h): plainstaticonPLATFORM_XBOX360 || PLATFORM_WEB,thread_localelsewhere. libxenon's_startnever establishes a thread pointer (r2 on PowerPC32) yet the ELF still carries aPT_TLSsegment for the engine'sthread_locals, so the first access dereferenced an uninitialised thread pointer — a silent memory fault during boot insideWidget::MarkDirty()(its recursion-depth guard) on the very first widget constructed, on emulator and hardware alike. Converted:Widget::MarkDirty'ssMarkDirtyDepthandsCycleWarnCount,AssetManager::DiscoverDirectory'ssDiscoverTickCounter(both files now includeSystem/SystemConstants.h). Degrades cleanly becauseSYS_CreateThreadcannot create a thread on those targets. Bullet gets the same treatment viaPOLYPHASE_NO_TLS, defined by the Xbox 360 build-target addon's makefile (not in-tree):btThreads.cppTHREAD_LOCAL_STATIC→static,btQuickprof.cpp#undef BT_HAVE_TLS.
Addon API
POLYPHASE_PLUGIN_API_VERSION8 → 9.EditorUIHooks::EditorImage_Load / GetSize / Invalidate(ABI-additive; engine-owned, no per-HookId release —RemoveAllHooksdeliberately leaves the cache alone).EditorUIHooks mHooks{}is value-initialised.New
POLYPHASE_APIexports:Capsule3D,InstancedMesh3D,Spline3D,Script::SetScriptProperties,AssetManager::GetAssetStubByPath / LoadAssetByPath,GFX_GetVulkanAddonHandles,ActionManager::Get / SaveScene / CreateNewScene / ImportScene,GetEditorState,EditorState::GetEditorCamera / OpenEditScene / SetAssetDirectory / GetAssetDirectory,EditorAddUniqueAsset,DrawAssetProperty,CreateAABBTableLua. The CI smoke addon (Tools/CI/TestBuildAddon/com.polyphase.smoke.material) exercises them.Skeletal introspection on the plugin C API — see Animation above.
*Exports (beta.17 / beta.18).* The
ContentPakmount-stack API,AssetManager::DiscoverAssetRegistry/RegisterAsset, theAssetDir,Sha256andCanvasclasses,ActionManager::OpenScene/ImportAsset/ImportAssetCombined/RetargetAnimation, andIsHeadlessService()are nowPOLYPHASE_API— see the Content Pak and Headless Service sections.Controller-server addon routes (beta.18) answer through a heap
std::vector<char>ofkAddonResponseBufferSize = 256 * 1024(ControllerServerRoutes.cpp) instead of achar responseBuffer[4096]on a Crow worker stack, passed asresponseBuffer.data()/(int32_t)responseBuffer.size(). Thecom.polyphase.format.io.glbchunked bundle download needed ~600 requests for a ~2 MB file at 4 KB. One heap allocation per addon-route request.*Android camera permission bridge (beta.18),
PolyphaseActivity.java.*private static final int CAMERA_PERMISSION_REQUEST_CODE = 4242,private volatile int cameraPermissionState = -1,public boolean hasCameraPermission()(checkSelfPermission(Manifest.permission.CAMERA)),public void requestCameraPermission()(short-circuits to1if already granted, else resets to-1and callsrequestPermissions),public int getCameraPermissionState()(-1pending,0denied,1granted),onRequestPermissionsResultoverride. Poll-based by design:requestPermissions()is asynchronous and native code cannot block a capture thread on a user dialog; the addon'sWebcamBackend_Android.cppcalls once and polls. Template manifest adds<uses-permission android:name="android.permission.CAMERA" />and<uses-feature android:name="android.hardware.camera" android:required="false" />/android.hardware.camera.any—required="false"because this is the shared engine template and camera-less devices must still install.**
InjectNativeAddonsIntoCmakeplatform key (beta.18).**ActionManager::BuildPhase1passed"android", butNativeModuleMetadata::ResolveExtrasmatchesnativePerPlatformkeys case-sensitively and every addon'spackage.jsonuses"Android", soextraDefines,extraLibsandextraLibDirswere dropped for every Android build on every project;extraIncludeDirsfrom a platform-common block still resolved, which masked it (headers compiled, defines did not). Now"Android". Editor-only code (#if EDITOR), so it needs an editor rebuild, not a repackage. Visible in the regeneratedAddonInject.cmake:POLYPHASE_ADDON_DEFINES→POLYPHASE_WITH_CAMERA2=1,POLYPHASE_ADDON_LIBS→camera2ndk,mediandk(bare names —POLYPHASE_ADDON_LIBSfeedstarget_link_librariesdirectly, never-lflags).
Engine Infrastructure
Portable
FileWatcher. The Windows-onlyReadDirectoryChangesW-over-IOCP implementation was replaced with a polling watcher built onstd::thread,stat()and theSYS_*directory API, so it behaves identically on every editor-capable platform.kPollIntervalMs = 500, sliced intokPollSliceMs = 50chunks soShutdownjoins promptly. NewFileSnapshot { lastSeenTime, lastEmittedTime, seenThisScan }andstd::atomic<bool> mNeedsRebaseline— the first scan after a watch-set change or a re-enable records mtimes without emitting, so re-enabling hot-reload does not fire a reload for every file edited while it was off. A change is emitted only once its mtime has held steady for a full interval, so a half-written file is never dispatched. NewUnwatchAll.Datumaddon-toolchain overload guard. TheDatum(int)overload condition becamePLATFORM_3DS || (defined(POLYPHASE_PLATFORM_ADDON) && !defined(POLYPHASE_ADDON_INT32_IS_INT)), adding an opt-out for addon toolchains whereint32_tisintand the extra overload would collide withDatum(int32_t).ENDIAN_SWAPauto-detects big-endian via__BYTE_ORDER__ == __ORDER_BIG_ENDIAN__in addition toPLATFORM_DOLPHIN || PLATFORM_N64, covering addon targets that have noPlatformenum value.ASSET_VERSION_CURRENTbumped 35 to 38 — 36 skeletal mesh sections, 37 bone mask, 38 soundwave streaming.Version string bumped to
6.2.0-beta.11inConstants.h.FileWatcher::WatchDirectoryextension filter (WatchDir::extensions, default{".lua"}, empty = all).SYS_CreateDirectory(Windows/Linux/Android/Dolphin/3DS): failures other thanEEXISTnow log a warning with the path; Windows no longer warns onEEXIST, and the POSIX platforms were previously silent.Engine.vcxproj/.filters: addedEditor\CSharp\CSharpManager.{h,cpp}(newSource Files\Editor\CSharpfilter),SpotLight3d.{h,cpp},SpotLight3d_Lua,WebSocket_Lua, and the sevenNetwork\WebSocket*files; both files gained a UTF-8 BOM.Version string in
Constants.his6.2.0-beta.18(bumpedbeta.16→beta.17on 2026-09-08 andbeta.17→beta.18on 2026-09-14).AssetManager::SaveAsset(beta.18) walksmParentDirupward collecting every missing ancestor intostd::vector<AssetDir*> missingDirsand creates them root-to-leaf. The previous single-levelCreateDir()onstub.mDirectory->mPathfailed withENOENTon the innermost path of a brand-newAssetDirchain (a cook pipeline creatingAssetIO/<uuid>/<mesh>/in one shot) and the asset silently never saved.**
Sha256forced linkage (beta.17).**Sha256(Engine/Utils/Sha256.h) isPOLYPHASE_APIbut has no Node/Asset registration, soFORCE_LINK_CALLnever covered it, and its only in-engine caller isEngineRuntimeValidator.cpp, whose chain is not reachable from every configuration. MSVC only pulls an.objout of a static library when something already linked references it, soSha256.objwas dropped and the export never existed —LNK2019onSha256::HashHexincom.polyphase.format.io.glb'sPlatformBundler.Engine::ForceLinkage()now includesUtils/Sha256.hand calls(void)Sha256::HashHex(&forceLinkSha256Dummy, 1)unconditionally. That in turn broke the 3DS, GameCube and Wii links: console Makefiles enumerateSOURCESexplicitly rather than globbing, andSource/Engine/Utilswas never listed. It now followsSource/Engine/Timeline/TracksinEngine/Makefile_3DS,Makefile_GCNandMakefile_Wii. Thepolyphase-buildtargetskill gained the rule with a verification snippet (five built-inEngine/Makefile_*plus eleven out-of-treeBuildTarget-*Makefiles; PSP and N64 carry two copies each; ordinary addons are unaffected because theyfile(GLOB_RECURSE)their own tree and link the prebuilt engine).*Android
Log To File(beta.18),Log.cpp.* New#elif PLATFORM_ANDROIDbranch inOpenLogFile()prefixes the log name withengineState->mSystem.mActivity->internalDataPath(app-private internal storage, no permission needed), mirroring thems0:/PSP/GAME/POLYPHASE/PSP branch. The relative path used before fails silently in a NativeActivity, which has no meaningful writable cwd, leavingmLogFilenull and no file anywhere. Retrieve withadb shell run-as <applicationId> cat files/<ProjectName>.log.
Addons and Build Targets
Everything below lives in its own repository under github.com/Polyphase-Labs and is discovered through the official registry (Polyphase-Engine---Official-Addons), whose auto-generated manifest.json the Addons Manager Browse tab now reads directly. Versions, plugin API levels and maturity are as of 2026-09-05. Items marked in progress boot and render but are not yet feature-complete; the commit log is the source for that judgement, since several READMEs lag their repositories.
Build Targets
- PlayStation 2 —
com.polyphase.build.target.ps2(v1.0.0, apiVersion 4, target idhomebrew.ps2, displaySony PS2 (PS2SDK + gsKit), categoryRetro Consoles). Compiles via PS2SDK 2024+ (mips64r5900el-ps2-elf-gcc) with gsKit and dmaKit throughMakefile_PS2;audsrv.irxis embedded into the ELF viabin2c. Output is a bare.elfplus an optionalmkisofs.iso(ps2.makeIso=1,-iso-level 1, so 8.3 filename mangling applies) for PCSX2 and FreeMcBoot. Windows native (ps2dev ships a Windows bundle), Linux/macOS native, WSL as an opt-in.Graphics_PS2GSrenders 640x448 NTSC interlaced (GS_PSM_CT24+GS_PSMZ_16S): textured and vertex-color static meshes, CPU-skinned skeletal meshes, instanced meshes (one draw per instance), 3D text, TileMap2D / Terrain3D / Voxel3D vertex-color meshes, particles (4→6 vertex expansion), UI widgets and font atlas. CPU per-vertex lighting with ambient, one directional, four point lights and — as of 2026-08-31 — spot lights, with GS overbright MODULATE; tint, emissive, vertex color, two-sided, four blend modes, masked blend mode and translucent depth sort (draw order, 2026-08-29); GS hardware fog plus a horizon-gradient skybox fog. Audio is an 8-voice plus 4-stream EE software mixer feedingaudsrv, paced byaudsrv_wait_audio; the mix loop is all-integer 16.16 fixed-point because the EE FPU is single-precision. Input is libpad DualShock 2 with pressure and vibration; saves go to memory card via libmc withicon.sys. Network is a stub. No shadows, post-processing or light bake. 32 MB EE RAM, embedded assets off,-DGLM_FORCE_UNRESTRICTED_GENTYPEfor the non-IEC559 FPU,-j4default because engine TUs peak 1–1.5 GB each. PAL andcdrom0:boot deferred. - PlayStation Portable —
com.polyphase.build.target.psp(v1.0.0, apiVersion 4,homebrew.psp,Sony PSP (PSPSDK), categoryHandheld). Compiles via PSPSDK (psp-gcc, linkspspgum_vfpu+pspvfpu) throughMakefile_PSP;PSPDEVresolved from the profile, the environment, then/usr/local/pspdev,/opt/pspdev,$HOME/pspdev,/mnt/c/pspdev. Packs ELF +PARAM.SFO+ XMB tiles (ICON0.PNG,ICON1.PMF,PIC0.PNG,PIC1.PNG,SND0.AT3) intoEBOOT.PBPviapack-pbp, with a Build-to-ISO mode that stagesPSP_GAME/. Launches in PPSSPP or deploys to hardware viapspsh. Linux/macOS native; Windows through WSL2 with automatic/mnt/cpath translation.Graphics_PSPGUis fixed-functionsceGu: textured static meshes (32-byte vertices),sceGuLight0..3hardware vertex lighting plus ambient, Lit/Unlit/Toon (Toon maps to Lit), four blend modes, back-face culling; textures are padded to power-of-two withSetUVMaxcropping and honour the asset's filter and wrap. Since the README was written the addon has gained ansceAudiomixer thread, CPU-skinned skeletal meshes repacked per frame, instanced meshes, particles, 2D UI with texel-space texcoords, save data underms0:/PSP/SAVEDATA/<discId>/, out-of-tree builds underIntermediate/PSP/, and — 2026-08-29 — Content Pak support. Constraints: 32 MB RAM (PSP-1000), 2 MB VRAM with roughly 1.3 MB left for textures after the 480x272 double buffer and 16-bit Z,doubleis software-emulated,sceGum*corrupts issued draws (usesceGuSetMatrix), no.ddependency tracking,-j4default. - Dreamcast —
com.polyphase.build.target.dreamcast(v1.0.0, apiVersion 4,homebrew.dreamcast,Dreamcast (KallistiOS), categoryRetro Consoles). In progress. Compiles viakos-c++throughMakefile_Dreamcast, out-of-tree inIntermediate/Dreamcast/, with three toolchain routes selectable in Target Options: DreamSDK on Windows viadreamsdk-runner.exe, a native KallistiOS install sourcing$KOS_BASE/environ.sh, or KallistiOS under WSL.GetCompileCommandwritespolyphase_build.shand runs it under the chosen shell. Output is a KOS.elfwrapped intoPackaged/homebrew.dreamcast/<name>.cdior.gdiviamkdcdisc(scramble → makeip →mkisofs -C 0,11702→cdi4dc), plus aPackaged/selfboot/folder with1ST_READ.BINfor mastering a retail-BIOS disc with BootDreams.Config.iniis rewritten to 640x480. Options: Region (NTSC-U / NTSC-J / PAL), Disc Format, Parallel Jobs. Runs in flycast (default,DC_EMULATORoverride), lxdream or redream; hardware viadc-tool-ip -xto a dcload-ip console. Commit history through 2026-07-22: builds and runs, PowerVR2 mesh rendering with scene and asset loading from disc, static meshes, skeletal meshes, UI, audio playback with a streaming path optimized to play any size clip, and point lights. OnlyEmbeddedScripts.cppis linked (16 MB RAM); assets load from/cd/. Logging goes to SCIF serial and fault PCs map throughsh-elf-addr2line. The README's platform matrix still describes the Phase 1 state and is out of date. - PlayStation 3 —
com.polyphase.build.target.ps3(v1.0.0, apiVersion 4,homebrew.ps3,Sony PS3 (PSL1GHT), categoryRetro Consoles). In progress. Cross-compiles for the Cell PPU via the ps3dev / PSL1GHT toolchain (ppu-gcc) throughMakefile_PS3, links a.elf, fake-signs it into a directly bootable.self, and wraps it into an installable.pkg(make_self_npdrm+sfo+pkg+package_finalize). Launches in RPCS3 or deploys viaps3load. Linux/macOS native; Windows through WSL2.Graphics_PS3RSXwith Cg vertex and fragment shaders for mesh, particle and UI, generated intoPs3Shaders_gen.h;Ps3LibcLocks.cppand acompat/shim cover toolchain gaps. Commit history 2026-07-18 to 07-23: bootable on emulator, engine loads with a test cube, scene and mesh loading, fog, UI rendering, a memory fix, and stabilization of UI-in-scene builds. Engine-side,Stream::Scanwas reworked to hand-parse the%[^=]=%[^\n]shape because PS3's newlibvsscanffaults on scanset conversions. - Nintendo 64 —
com.polyphase.build.target.n64(v1.0.0, apiVersion 4,homebrew.n64,Nintendo 64 (libdragon), categoryRetro Consoles). In progress. libdragon toolchain throughMakefile_N64;Runtime/N64/carriesGraphics_N64RDP,Audio_N64,Input_N64,Network_N64,System_N64, aN64SoftMath.csoftware-math shim and aStdThreadingStub.h. Commit history 2026-05-23 to 05-26: packages to a ROM and draws, debug rotating cube, static meshes load and tick, static meshes lit. Textures not yet. - Original Xbox —
com.polyphase.build.target.xbox(v1.0.0, apiVersion 4,homebrew.xbox,Original Xbox (nxdk), categoryRetro Consoles). In progress. nxdk toolchain throughMakefile_Xbox;Runtime/Xbox/carriesGraphics_NV2Awith Cg vertex and pixel shaders, audio, input, network and system backends, and aCompat/libc shim. Single commit 2026-07-23: loads scenes with directional lighting, static meshes and textures; geometry still broken. - Nintendo DS —
com.polyphase.build.target.nds(v1.0.0, apiVersion 3). Scaffold only. A singleSource/ComPolyphaseBuildTargetNds.cppand package metadata; nobuildTargetsentry, no runtime. - Web (WebGL2) —
com.polyphase.build.target.webgl2(v1.0.0, apiVersion 4,web.webgl2,Web (WebGL2), categoryWeb). Compiles the engine plusRuntime/Webinto a single-threaded WebAssembly module with Emscripten (emcc,emmake,Makefile_Web), packages cooked content (Content.pak or loose tree) into a MEMFS preload bundle withfile_packager.py, and servesPackaged/web.webgl2/(index.html,<name>.html/.js/.wasm,<name>.data) onhttp://localhost:6931/. Runs from any static host — no SharedArrayBuffer, COOP or COEP. Options: Optimization (O0/O2/O3/Oz), Initial Memory (256 MB floor, growth on), Parallel Jobs (8), Server Port, Network Relay URL, Makefile override. Embedded profiles are rejected; Lua is always embedded in the wasm. Windows native (emsdk probed from%EMSDK%,C:\emsdk; needs GNU make from devkitPro MSYS2 orC:\msys64), Linux/macOS native, WSL fallback. Renders through WebGL2 / ESSL 300: static meshes, CPU-skinned skeletal meshes, 3D text, particles, UI, debug lines, fog, ambient + directional + point lights; shader-graph materials degrade to standard shading; voxel, terrain, tilemap and light baking not implemented. Audio via Emscripten OpenAL (static, panning, pitch, loop, streaming; starts after a user gesture). Input: keyboard, mouse with pointer lock, touch, gamepads. Saves are session-local MEMFS (IDBFS planned). HTTP viafetch()inHttp_Web.cpp(noHttp::SendSync, CORS applies,VerifySslandMaxRedirectsignored). Multiplayer tunnelsNET_*over WebSocket to the Node 16+ relay inTools/relay/(wss://under https). The engine-side prerequisites —AssetManagerthreadless drain,InputConstants#ifndefguards,PLATFORM_WEBexclusion from the HTTP stub — landed in the beta.11 gap. - Web (WebGPU) —
com.polyphase.build.target.webgpu(v1.0.0, apiVersion 4,web.webgpu,Web (WebGPU), categoryWeb). Same Emscripten pipeline withMakefile_WebGPUand--use-port=emdawnwebgpu(Dawn; the first build fetchesemdawnwebgpu_pkgonce). Staging underBuild/WebGPU/Intermediate/WebGPU, port 6932, options underwebgpu.*; coexists with the WebGL2 target in one project. Requires Chrome/Edge 113+, Safari 18+ or Firefox 141+; unsupported browsers get a shell message.Graphics_WebGPUimplements the 91-functionGFX_*surface with WGSL ubershaders inWebGPUShaders.h: static meshes, CPU-skinned skeletal meshes, text meshes, terrain, tilemaps, particles, UI, debug lines, fog, ambient + one directional + eight point lights, unlit / dynamic / baked light modes, blend modes, alpha masking. Stubs: shadows, voxels, splats, post-process, hit-check, GPU timestamps; no mipmap generation. The async device is acquired inModule.preRunbehind a run dependency; one render pass per frame inGFX_EndFrame; per-draw uniforms on a 256-aligned dynamic-offset ring in 256 KB chunks; pipeline cache keyed on program, layout, blend, depth and topology (about 15 pipelines); TRIANGLE_FAN expanded on the CPU. - Android TV —
com.polyphase.build.target.androidtv(v0.1.0, apiVersion 4,polyphase.androidtv,Android TV, categoryAndroid). Stages the engine'sStandalone/Androidproject intoIntermediate/BuildTargets/AndroidTV/, appliesOverlay/AndroidManifest.tv.xml(LEANBACK_LAUNCHER, touchscreen not required, leanback feature,@drawable/tv_banner), patchesbuild.gradleandstrings.xml, runsgradlew assembleReleasenatively, and copies the APK to<project>/Build/AndroidTV/<projectName>-android-tv.apk(.aabviabundleRelease). Options: Application ID, App Label, Version Name/Code, Gradle Task, Signing Mode (debug), Min/Target/Compile SDK (24/33/33), TV Banner PNG, SDK/NDK roots, Device Serial; Force Rebuild wipes staging. Run on Device issuesadb install -randadb shell monkey. Needs Android SDK, NDK, JDK 17+; on Windows needs Git Bash or WSL for the build script. Release keystore signing not wired;.aabinstall needs bundletool. - Android Universal —
com.polyphase.build.target.android.universal(v0.1.0, apiVersion 4,polyphase.androiduniversal,Android Universal, categoryAndroid). Derived from the TV addon with the overlay removed; patchesabiFiltersand buildslib/arm64-v8a/libstandalone.soandlib/armeabi-v7a/libstandalone.sointo one APK at<project>/Build/AndroidUniversal/<projectName>-android-universal.apk. Default ABI Filtersarm64-v8a,armeabi-v7a, overridable toarm64-v8aorarm64-v8a,x86_64. On Windows staging can move to%LOCALAPPDATA%\PolyAndroidUniv\<projectName>\Androidto dodge MAX_PATH during CMake/Ninja. FixesINSTALL_FAILED_NO_MATCHING_ABISon 32-bit-only Allwinner, Rockchip and MediaTek tablets; the native payload roughly doubles, andarmeabi-v7acan expose 32-bit compile errors in third-party C/C++. The engine's Android backend is Vulkan-only, so pre-Vulkan devices will install but not run. - Linux ARM64 —
com.polyphase.build.target.linux-arm64(v0.1.0, apiVersion 4,polyphase.linux.arm64,Linux ARM64 (aarch64), categoryLinux,native.target=engine). Cross-compiles the existing Linux + Vulkan runtime withaarch64-linux-gnu-*against a user-generated Debian 12 sysroot (Scripts/make-debian12-arm64-sysroot.shviammdebstrap+qemu-user-static; includeslibvulkan-dev,libxcb1-dev,libasound2-dev,libpulse-dev,libshaderc-dev), passingBUILD_SUFFIX=/arm64so outputs land inStandalone/Build/Linux/arm64/Polyphase.elfand never collide with x64. Options: Sysroot path, Cross prefix, Parallel jobs (8), Extra CFLAGS/LDFLAGS (e.g.-mcpu=cortex-a76), Strip binary, WSL Distro.PostPackageoptionally strips and writesLINUX_ARM64_README.txt. Targets Raspberry Pi 4/5 with Mesa Vulkan, Orange Pi 5, Pine64 and similar. Debian/Ubuntu native (crossbuild-essential-arm64), macOS viamessense/macos-cross-toolchains, Windows via WSL2. The main trap is glibc/libstdc++ skew — modern hosts emitGLIBC_2.38/GLIBCXX_3.4.32references bookworm lacks; the recommended fix is Bootlin'saarch64--glibc--stable-2024.02-1toolchain viaScripts/install-bootlin-toolchain.sh. No ARM64 editor; no newPlatformenum value. - Project ZIP Archive —
com.polyphase.build.target.zip.archive(v1.0.0, apiVersion 4,archive.zip,Project ZIP Archive, categoryArchive). Zips the open project into<project>/Packaged/archive.zip/<projectName>.zip(optionally_YYYYMMDD_HHMMSS) with a vendored miniz-based library; always excludes.git/,Packaged/,Intermediate/,Build/. Options: Use.gitignore(on; supports plain names,*.ext, trailing-slash directories and leading/anchors, not!negation or**), Compression 0–9 (6), Include Assets (on). Because the packaging pipeline has no skip-compile hook,GetCompileCommanddrops a ~2 KB placeholder atBuild/archive.zip/<projectName>.zipto satisfy the post-compile integrity gate andPostPackageoverwrites it with the real archive;basePlatformisLinuxsolely to skip the Windows-only PE check. Requires apiVersion ≥ 4.
Gameplay Systems
- FPS Framework —
com.polyphase.engine.fps(v1.0.0, apiVersion 4; depends oncom.polyphase.engine.combat.core ^1.0.0andcom.polyphase.system.rpg.mission.core ^1.0.0; optional integration with Dialogue Core). Input bindings for Shoot1/2/3, Reload, weapon swap, pause and Z-lock; navmesh-driven enemy AI; missions with story briefs; main, pause and settings menus. C++MissionRegistryandFps_Luabind theFPSglobal. Lua modules:FPSFramework.lua,FPSInput.lua,WeaponArchetypes.lua(Pistol, Rifle, BurstRifle, Shotgun, SMG, Sniper),EnemyArchetypes.lua(Grunt, Veteran, Sniper, Shotgunner, Heavy),Mission.lua,StoryBrief.lua. API:FPS.SetupDefaultInput(),FPS.Input.Rebind(ctx, action, bindings),FPS.IsZLockEnabled / ToggleZLock / SetZLockTarget / GetZLockTarget,FPS.GetSetting / SetSetting,FPS.GetGameState / SetGameState / TogglePause,FPS.Mission.*,FPS.StoryBrief.Show{title, text, speaker, duration} / Peek / Pop. Reference node-script classesFrameworkPlayer,FrameworkWeapon,EnemyAI(Idle → Patrol → Chase → Attack → Dead withmoveSpeed,sightRange,accuracytunables),MainMenu,PauseMenu,SettingsMenucopy into<Project>/Scripts/FPS/because the node-script picker only scans the project. - Combat Core —
com.polyphase.engine.combat.core(unpublished). Damage, hit resolution and combat state shared by the FPS and RPG systems. Listed in the registry as a dependency; the repository is currently empty. - Mission Core —
com.polyphase.system.rpg.mission.core(v1.0.0, apiVersion 4). Genre-neutral mission framework: C++MissionRegistry,MissionAsset,MissionAssetRegistry,Mission_Lua. Runtime:Mission.Register{id, title, brief, objectives},Unregister,Get,List,SetObjectiveState(missionId, objId, "Active"|"Completed"|"Failed"|"Hidden"),AddObjectiveProgress,AreAllObjectivesDone,ClearRuntime. Sugar inScripts/Mission.lua:Mission.Define{...},Start,Progress,SetObjective,Complete,Fail,Current,AutoCountKillsOnObjective(objId, predicate). Assets:Mission.GetAsset(id)(id,title,brief,briefSpeaker,sceneName,order,locked,objectives[],loadout[]),ListAssets,LoadAllAssets; flow inScripts/MissionFlow.lua:Mission.Flow.Pick / Accept / Begin / Cancel / CurrentMissionId. SignalBus eventsMission.Started / Completed / Failed / ObjectiveProgressed / ObjectiveChanged,Mission.Briefing.Requested / Accepted / Cancelled,Mission.Flow.Started / MissionCompleted. Editor: Asset Browser → New → Mission → MissionAsset with Mission, Scene, Objectives and Loadout sections.OnUnloadclears both registries; the Lua global is rebuilt on every load. - Items Core —
com.polyphase.system.items.core(v1.0.0, apiVersion 4).ItemInfoasset (schema v1; Identity, Behavior, Category, Signals, FX sections: name, category,autoUse, per-category effect parameters, icon, world mesh, pickup sound and VFX) backed by a C++ItemInfoRegistrywith live-pointer lookup, eager-loaded on first tick in shipped builds. Lua:Items.GetItemInfo(name)→displayName,category,amount;Items.ListItems;Items.CategoryandItems.Apply(player, itemName, count)in the project-sideScripts/FPS/Items/Items.luasugar layer so Heal / Ammo / Shield / Weapon handlers are overridable. EmitsFPS.Item.Pickup/FPS.Item.Usedon the SignalBus. Create Asset > Items > ItemInfo. - RPG Loot —
com.polyphase.system.rpg.loot(v1.0.0, apiVersion 4; talks to Items Core by item-name strings at the Lua layer, no C++ dependency).LootEntryasset (Item Name, Weight, Min/Max Count, condition tag) andLootTableasset (schema v2: Table Name, Rolls, Roll With Replacement, guaranteed and weightedLootEntryarrays; v1 parallel-array shape rejected). Lua globalLoot: C++GetTable,ListTables,HasTable; project-sideScripts/FPS/Loot/Loot.luaaddsLoot.Roll(tableName, { tags = {...} })→{item, count}list,Loot.GiveReward(player, tableName, ctx),Loot.SpawnPickup(itemName, count, position),Loot.AddToTableoverlays. Create Asset > Loot > LootEntry / LootTable; dockable Window > Loot Table Editor. Roll logic deliberately lives in Lua for debuggability. - Dialogue Core —
com.polyphase.system.character.dialogue.core(v0.1.0, apiVersion 3; ships a prebuilt Windows x64 DLL as a release asset; optional companioncom.polyphase.system.character.dialogue.inkconverts compiled*.ink.jsonto.dialogueat editor time).DialogueAssetimported from.dialogueJSON (start,speakers[]withid / name / portrait,nodes[]withid / type / speaker / portrait / text / jumpTargetId, node typeslineandend).DialogueRunner3D(Node3D) drives a conversation and emits signals (runner:StartDialogue());DialogueBoxWidgetrenders line, portraits, choices and press-to-continue, bound via aRunner Pathproperty. Lua: instance methods plus a globalDialogue.*module (Dialogue.SetGlobalStringis referenced by the FPS addon). Editor: a preview window that steps through a dialogue without a scene, and a validator for missing speakers, unreachable nodes and broken jumps. Console-safe by construction (no RTTI, no exceptions); not yet validated on 3DS/Wii/GameCube hardware. Deferred: visual graph editor, NodeGraph dialogue nodes, Yarn / full Ink runtime bridge. TheLanternsAndLies-Demo-Gamerepository is built on it. - World Stream —
com.polyphase.system.worldstream(v0.1.0, apiVersion 3; Windows, Linux, GameCube, Wii, 3DS). Sector streaming:WorldStreamSource3D(attach to the player, camera or party leader),WorldStreamSector3D(ModeDistanceRadius or TriggerVolume, radii, extents,Scene Override,Always Resident), and aWorldStreamSectorasset (sectorId,sceneName,priority,unloadMode,alwaysResident,memoryKB) importable from akey=value.wssectorfile. Sectors load when a source enters the load radius and unload afterUnload Delayseconds outside the unload radius. Lua:WorldStream.SetEnabled(bool),SetCallback("OnWorldStreamSectorLoaded", fn(sectorId)),LoadSector(id, bool),UnloadSector(id),UnloadAll(),GetStats()→activeSectors,queuedSectors,estimatedMemoryKB. Editor: Create Asset → WorldStream → World Sector; View → World Stream window with platform policy, sector state, distance to nearest source and manual load/unload. Console defaults: one load and one unload per frame, two active sectors.
Formats
- Video Player —
com.polyphase.formats.video(v1.0.0, apiVersion 3; Windows, Linux, GameCube, Wii, 3DS). Announced in beta 9; since then it has gained Wii/GameCube playback via built-in software decoders (PCV1 recommended; THP plays with limited audio because the DSP-ADPCM LPC encoder is disabled in favour of Nintendo's fixed coefficient table), 3DS PCV1 playback, an editor-only cook step with Tiny / Balanced / Quality / Custom presets and a Test Cook for Platform action, and audio as the master clock so loop and sync behave identically everywhere.VideoClipasset (.mp4 / .mov / .webm / .mkv / .avi / .m4v),VideoPlayer3Dnode withSetVideoClip,SetLoop,Play,GetTextureand anOnReadysignal. Windows uses a vendored LGPL FFmpeg shared build; Linux via pkg-config (FFmpeg ≥ 4.0). N3MV (3DS hardware H.264) cooks but its runtime decoder is scaffold-only. - MIDI —
com.polyphase.formats.midi(v1.0.0, apiVersion 3; Windows, Linux, 3DS, Wii, GameCube).MidiSequenceasset (.mid / .midi / .kar) andSoundFontBankasset (.sf2), both registered viaRegisterImportExtension.MidiPlayer3Dnode:SetMidiFilePath,SetSoundFontFilePath,SetLooping,SetVolume,Play / Pause / Stop / SetTime, liveNoteOn / NoteOff / SetProgram, and a procedural builderMidi.CreateSequence{...}; path setters tryLoadAsset(name)first. Backend is embedded TinySoundFont (MIT) plus the midifile parser (BSD-2), onetsfinstance per player so console voice budgets stay enforceable; aMidiSequencerworker thread pushes PCM at 50 chunks/s viaAudio_SubmitStreamBuffer. Per-platform caps viaPOLYPHASE_MIDI_MAX_VOICES/SAMPLE_RATE/CHANNELS: 3DS 24 voices at 22050 Hz, Wii and GameCube 16 voices at 32000 Hz. Editor: View → MIDI → Asset Viewer, SoundFont Viewer (preset table with click-to-preview keyboard), Piano Roll (note rects, zoom, pan, channel filter, transport, playhead); inspector toggles Pin in Piano Roll and Use for Preview. - Webcam —
com.polyphase.formats.webcam(v1.0.0, apiVersion 3).WebcamPlayer3Dnode opens a device, runs a background capture thread and pushes RGBA frames into aTexture:SetCameraName(substring),Open(),Start(),GetTexture(),GrabFrame()(a stable, independentTextureper call); signalsOnReady,OnError; inspector properties for camera by name or index, preferred resolution and FPS, mirror X, flip Y. Connector scriptsWebcamQuadConnector.luaandWebcamMaterialConnector.lua. Windows: Media Foundation (RGB32 / NV12 / YUY2 with MF-side conversion). Linux: V4L2 with mmap, YUYV and RGB24 only (MJPEG-only cameras failOpen). 3DS: libctru CAM (Inner, Outer-Left, Outer-Right, Outer-Stereo), verified on retail hardware. Wii/GameCube: compile-clean stubs that emitOnError. Editor: Developer → Webcam Devices window. Hot-reload safe — threads, devices and textures are torn down before DLL unload. - Gaussian Splatting —
com.polyphase.formats.gaussiansplat(v0.1.0, apiVersion 4; repository currently private).GaussianSplatAssetfrom.ply(Inria: position, opacity, scale, rotation, SH0 colour) and.splat(Antimatter15 32-byte binary);.ksplatrejected. OwnGSPLAT_ASSET_VERSION_CURRENT, independent of the engine's.GaussianSplat3Dnode under Add Node → VFX with a Splat Asset slot and Max Visible Splats (default 20000). Lua:GaussianSplat.Load / Unload / IsLoaded / GetStats(liveNodeCount,visibleSplats,cpuMemoryKB) /SetGlobalSplatCap. Editor: Tools > Gaussian Splatting > Bake Collision Proxy / Bake Console Fallback / Log Splat Stats; File > Import Gaussian Splat. Windows/Linux build withPOLYPHASE_GSPLAT_VULKAN=1; GameCube/Wii/3DS withPOLYPHASE_GSPLAT_FALLBACK_ONLY=1(metadata loads, renderer no-ops). The README still describes the MVP renderer (one debug cube per splat, CPU back-to-front sort, cap 4000), but a 2026-09-04 commit adds a compute-based tile rasterizer (TileRasterizer.cpp, ~1,070 lines) alongside the existing splat renderer —SplatTilePreprocess,SplatTileEmit,SplatTilePrefix,SplatTileScatter,SplatTileRendercompute passes and aSplatTileCompositefullscreen pass — with node and Lua bindings to select it. SH bands 1–3 dropped on save; collision bake is bounds-only.
Editor Tooling
- Level Builder Core —
com.polyphase.editor.levelbuilder.core(v0.1.0, plugin apiVersion 3, Level Builder C ABI v13; hot-reload safe). The framework half: the dockable Level Builder window (Brush / Palette / Placement / Debug tabs plus dynamic extension tabs registered by siblings; the static Mode tab was retired in favour of viewport-mode activation through the engine'sAddViewportModehook), a separate dockable Kit Editor window (kit metadata, piece add/remove, per-piece editor, Save), name-keyed tool / brush / snap-provider / palette registries, the core-ownedLBKitRegistry(every kit under<project>/Kits/and<project>/Assets/Kits/, folder-modekit.json+pieces/*.jsonstorage,.kitzip import/export via bundled miniz), viewport preview and raycast dispatch, and the brush spawn-function router. Siblings resolve the single exportLevelBuilderCore_GetAPIviaGetProcAddress/dlsymwith a strictapiVersion == LEVEL_BUILDER_CORE_API_VERSIONcheck; a mismatch silently disables the sibling's tab, so every sibling must be rebuilt after a core rebuild. ABI history: v2 viewport hover/click, v3 core-owned kit registry, v4 spawn-fn registry, v5 socket mutation, v6 kit sharing metadata and.kitpack/unpack, v7 paint-brush drag input and placed-piece enumeration, v10 piece-level mutation (Kit_AddPiece/Kit_RemovePiece), v11sourceAssetFilteron enumeration, v12 asset name inLBVisitFn, v13RegisterRebuildFromWorldFn. - Level Builder Tools —
com.polyphase.editor.levelbuilder.tool.core(v0.1.0, apiVersion 3). Generic brushes — Line, Box, BoxFill, NoiseFill, Replace, Paint, MaskFill — that route placements through the active sibling's registered spawn function so they stay kit-agnostic and work in both Modular and Grid modes. - Modular Level Builder —
com.polyphase.editor.levelbuilder.modular(v0.1.0, apiVersion 3; depends on Level Builder Core; not yet in the registry manifest). Registers toolModular Placement, brushModular Single, snap providerModular Socketand paletteModular. The single-piece brush spawns aStaticMesh3DforStaticMeshassets or a full prefab viaWorld::SpawnSceneforSceneassets. The socket-aware snap provider picks the closest compatible socket on a placed piece and aligns the active piece's matching socket to it; R-yaw is absolute world facing; theRkey rotates 90° (gated on!ImGui::GetIO().WantTextInput). Modular tab: kit dropdown with Reload, project-root override and New / Rename / Delete; thumbnailed piece cards; Piece Properties (name, category, icon, size, sockets, Add / Remove Piece, Auto-Detect Size, Apply / Save Kit / Revert) with a drag-target asset slot routed throughKit_SetPieceAsset; a sockets sub-editor; rotation slider; click-in-viewport placement with a World Pos fallback; Debug toggles for sockets, bounds and names. With Edit Sockets on, each socket gets a three-axis transform gizmo (red X, green Y viaViewport_GetMouseWorldRay, blue Z, magenta centre for free X/Z drag) pushing throughKit_SetSocketPosition. A persistent placed-piece registry rebuilds from the live scene tree on mode activation via the v13 hook. Kit storage via core: folder-mode (preferred), single-file legacy JSON (auto-upgraded on first save), Polyphase-native Kenney packs (<Kits>/<Pack>/Models/Polyphase/*.oct+Previews/*.png, auto-recognized), and.kitzip bundles. - Grid Level Builder —
com.polyphase.editor.levelbuilder.grid(v0.1.0, apiVersion 3; depends on Level Builder Core; not yet in the registry manifest). Uniform-grid block placement — Roblox / Minecraft-style, or prop-on-grid for top-down and first-person dungeon kits. Registers toolGrid Placement, brushGrid Single, snap providerGrid Snap(configurable XYZ cell size, no sockets). Grid tab: kit dropdown sharing core'sKit_*registry, cell-size sliders, grid overlay toggle, text-list piece picker; viewport overlay draws wire cubes at each cell within a camera-relative radius. RegistersGridSpawnAtTransformso everytool.corebrush works in Grid mode, and rebuilds its placed-piece registry from the world on activation. Missing: per-grid kit subset flag, configurable rotation snap (90° Y only), separate vertical cell size, per-piece collision/overlap toggles, thumbnail picker. - Houdini Engine and HDA —
com.polyphase.thirdparty.houdini.hda(v1.0.0, apiVersion 9,native.target=editor; Windows only). Imports.hda / .hdalc / .hdanc / .otl; a Houdini main menu with Houdini Engine (session, installs, conversion settings, log) and HDA Instances (libraries, parameters, inputs, bake) windows, plus Houdini → Load HDA. LoadslibHAPIL.dllat runtime with version-selectedHapi21Backend/Hapi20Backend(Houdini 21.x / Engine 8, or 20.0 / Engine 6.2; Engine or full license, not Apprentice), all HAPI calls on one worker thread, headless HARS over named pipe or SessionSync over pipe/TCP. Inputs:StaticMesh3D, Node3D groups (up to four inputs),StaticMeshassets,Spline3Dcurves, point welding, vertex normals and UVs,Cd/Alpha, materials viashop_materialpath/polyphase_material, optional texture-to-Cdsampling. Output: mesh parts →StaticMeshassets andStaticMesh3Dnodes, curves →Spline3D, point-cloud instancing viapolyphase_asset_path(collapses toInstancedMesh3Dfor four or more uniform-scale points). Nothing Houdini-specific persists into scenes or builds; no Lua or REST surface. Requires an engine built after 2026-08-01 forSpline3Dexport. Latest commit adds vertex-color input.
Registry, Examples and Agents
- Official registry —
Polyphase-Engine---Official-Addons.package.jsonlists the addon repositories,registry.config.jsoncarries categories and hand-authored overrides, andtools/build-manifest.mjs(Node 18+) pulls each repo'spackage.jsonplus GitHub metadata intomanifest.json, rebuilt by a workflow on every commit, nightly, onworkflow_dispatch, and on anaddon-updatedrepository dispatch that an addon repo can fire the moment it publishes. Categories: Examples, Systems, Editor, Formats, Build Targets, Third Party.CreateYourOwnAddonsRepo.mddocuments standing up a studio, team or personal registry with the same tooling, including the GitHub Actions permissions the workflow needs. - Examples.
Hello-World-Polyphase-Addon(a single Text Widget),Voxel-Builder-Polyphase-Example,VideoPlayer-Polyphase-Demo,LanternsAndLies-Demo-Game(Dialogue Core showcase),Websocket-Client---PolyphasewithWebsocket-Server---Basic-Example(the WsEchoTest pair),3DS-Screens-Example---Polyphase(separate scenes per screen, with screenshots),Serial-Connection-Polyphase-Example. - Agents.
Polyphase-InfoAgent-OpenClaw, an OpenClaw information agent for the engine, andPolyphase-CodeAgent-OpenClaw, a coding agent that creates feature branches and pull requests against it.
Bug
WAV files imported silent or as garbage.
SoundWave::Importread the data size at fixed offset 40 and the PCM at 44, assuming thedatachunk immediately followedfmt; DAWs and C2PA tools insertLIST/INFOchunks before it. The importer now walks word-aligned chunks through the sharedParseWav, and asserts became logged failures.Stereo durations reported double.
mNumSamplesis interleaved, butGetDurationdivided by the sample rate as if it were frames. Now usesGetNumFrames(); the same fix applied toXAUDIO2_BUFFER::PlayBegininAudio_Windows.cppandAudioManager::GetVoicePcmInfo.Nearest-filter textures rendered as linear on 3DS. Three causes:
Texture::IsMipmapped()/GetMipLevels()still generated averaged mips forFilterType::Nearest, which blur pixel art when minified;Graphics_C3Dused aGPU_LINEARmip filter, so trilinear made Nearest look Linear at 400x240; and the cook and LQ-downsample path (ResizeForCook) smooth-resampled Nearest textures instead of point-sampling. All three now honour Nearest. Font atlases:Filter Type/Wrap Mode/Mipmappedchanges now reach the baked texture viaTexture::RecreateResource(), andLoadStreamre-applies the font's filter and wrap over stale atlas settings;Font"Mipmapped" usedHandleAssetPropChangeand now usesFont::HandlePropChange.Creating a project crashed the Linux editor (
ImGui_ImplVulkan_RemoveTexturemid-frame).GamePreview::CreateRenderTargetsandDisable, andSecondScreenPreview::Disable, freed the ImGui descriptor set immediately;ApplyProjectDefaultsreaches that path fromOpenProjectafterDrawPanelhas already queued anImGui::Imagewith the old handle, so the backend bound a dead set. RADV segfaulted; Windows tolerated it. NewEditorImageCache::RetireTexture()defers release to the next frame'sRetirePending.BuildTargetRegistrystring pointers dangled on Linux.emplace_backreallocation left earliermDescstring pointers pointing into moved-from small-string storage;RepointDescriptorStrings()now runs after every register and unregister.Scene save and open were pathologically slow with linked scenes.
GatherSubSceneOverridesinstantiated the linked scene once per visited node, so a large prefab triggered hundreds of full builds and a progress bar that appeared to loop. One default instance is now shared per subtree (Scene::AddNodeDef,Node::Clone,CacheEditSceneLinkedProps) via the newdefaultSceneRootparameter.Headless packages shipped no engine textures.
ImportEngineAssets()was skipped when headless, so PNG-backed engine textures had no stubs to cook and the game crashed on its first UI quad with a nullmWhiteTexture. It now runs headless too.A texture cook flipped every image written after it.
stbi_flip_vertically_on_writeis global and was left set, so later SMDH icon, CIA banner and Android icon writes came out upside down. Reset after the write.Editing the 3DS title or icon reported "up to date".
BuildCache's manifest key returned""for canonical targets; it is now"opts"plus the option hash whenever options exist.Addon builds failed on
vulkan/vulkan.hwith no hint when%VULKAN_SDK%/$VULKAN_SDKwas unset and expanded to literal text. Generated scripts now embedResolveVulkanIncludeDir()and preflight rejects the build with an explanation.Texture fixup modal saves were silent no-ops.
TextureImportFixupModalcalledGetAssetStub(name + ".oct"), but the asset map is keyed by bare name. Fixed toGetAssetStub(name).Keyboard axis emulation was inverted. Stick Y is up-positive;
W/Imoved to the positive keys,S/Kto negative.Startup asserted when Vulkan validation layers were unavailable.
VulkanContext::CreateInstancenow warns and continues, which is what a shipped.appwith no SDK on the layer path needs.InputPromptMap"Any" entries became Mac.Platform::Countwas 7 before Mac was appended; pre-v43 files remap raw7toPlatform::Count.The gamepad axis
+button was unclickable inInputMapWindow::DrawGamepadAxes(column width 70 → 150). The addon shadow-copy retry ran anrmdirevery frame while a.pdbwas pinned; now paced and bounded.BuildDependencyWindow::CheckDevkitToolchaincatchesDEVKITPPCpointing at\bin, previously a cryptic make failure.Script hot-reload did nothing after opening a project.
mScriptHotReloadis per-project, read from the project'sConfig.ini, butFileWatcher::mEnabledwas synced from it exactly once at engineInitialize()— which runs before any project is open. Opening a project with hot-reload enabled left the watcher queueing events thatUpdate()discarded every frame, while the preferences checkbox showed it as on.LoadProjectnow callsSetEnabled(IsScriptHotReloadEnabled())before registering the Scripts watch, and skips watching entirely when headless. Launching with a-projectargument masked the bug, because the project config was read early enough for the one sync to catch it.Hot-reload watches stacked across project switches.
CloseProjectnever unwatched, so eachLoadProjectappended the project Scripts tree and every addon Scripts tree again, and closed projects kept reporting paths.CloseProjectnow callsFileWatcher::UnwatchAll.The App Settings
Script Hot Reloadcheckbox never reached the live watcher. It wroteconfig->mScriptHotReloadin place, bypassingSetScriptHotReloadEnabled— the only path that pushes state into the watcher — so it took effect on the next editor launch and disagreed with the identical checkbox inPreferences > General. Now routed through the setter.Builds under OneDrive or Controlled Folder Access silently shipped truncated packages. File writes failed without surfacing an error, producing empty embedded assets that booted to a black screen on GameCube and Wii, where there is no filesystem fallback. New
VerifyBuildDirWritablecreates the directory, writes and deletes a one-byte probe, andBuildPhase1runs it against both the packaged directory andIntermediate/before the cook, aborting with a message naming OneDrive and Controlled Folder Access as the likely cause. Breaking for out-of-tree platform backends:SYS_CopyFilechanged fromvoidtoboolacross Windows, Linux, Android, Dolphin and 3DS, each confirming the destination exists after the copy, becausecopyis a CMD intrinsic whose exit code is unreliable andcpruns throughSYS_Exec.BuildPhase1accumulates the result over the four.octpandConfig.inistagings and aborts on failure. The default new-project path also moved offDocumentsto%USERPROFILE%/PolyphaseProjects, which is not OneDrive-redirected.Headless cooks segfaulted right after
Cooking assets.... Engine-asset discovery was gated on!mUseAssetRegistry, butAssetRegistry.txtis an output of packaging and never an input to the cook. With the flag on, the root asset directory had noEnginechild andBuildDatadereferenced a nullFindEngineDirectory(), leaving a truncated build log as the only clue. Discovery is no longer gated on the flag, with a null guard in the save lambda and an explicit readable failure message.Docker builds linked stale host object files.
External/Bullet/Intermediate/andExternal/Assimp/Intermediate/were copied into the container with mtimes preserved, so containermaketreated host-compiled objects as up to date and the Makefile wildcard globs linked them in. When the host ran a newer toolchain the objects referenced symbols absent in the container image and the editor link failed. Both paths added to.dockerignore.Addon-target console builds fell back to directory scanning. Console builds force the asset registry on for load speed, but the check was a
Platformenum switch over Android, GameCube, Wii and 3DS — addon-target consoles arePOLYPHASE_PLATFORM_ADDONbuilds that match none of those. On PSP that is not merely slow:sceIoDreadsilently drops entries when file I/O is interleaved, so some assets never registered and the configured default scene was not found.ReadEngineConfignow forces the registry on for any addon-platform build.Consoles booted with no UI when deployed into a per-project folder. Shipped engine content sits at
Engine/<sub>relative to the working directory, but console packages are routinely deployed into a per-title folder on an SD card, which putsEngine/inside the project folder while the working directory stays at the card root — so every engine asset, including default textures, materials and fonts, was missing. Resolved through the newGetEngineContentDir. Embedded builds masked it.Packaged console builds could not find their own project file.
BuildPhase1staged<project>.octpandConfig.inionly to the package root, while the standaloneLoadProject()opens<projectName>/<projectName>.octp. Both files are now staged to both locations.AssetManager::DiscoverAssetRegistrymis-resolvedEngine/paths on per-project console deploys. Registry lines beginning withEngine/are now rewritten throughGetEngineContentDir. Only the on-disk path is rewritten;RegisterAssetderives the lookup name from the basename, so name-based lookups are unaffected.Unreadable assets tripped a bounds assert.
DiscoverDirectoryandRefreshDirectorycalledAsset::ReadHeaderwithout checking theStream::ReadFileresult, so an empty stream faulted insideStream::Read. Both now check first and logAssetManager: skipping unreadable asset '%s'.DataAssetscripts failed to load in packaged builds.LoadPropertiesFromLuausedluaL_dofile, which bypasses both the embedded-script table and Static Content decoding. Now reads throughStreamandluaL_loadbuffer/lua_pcall.PS3 faulted parsing
Config.iniand.octp. PS3's newlibvsscanffaults on the%[...]scanset conversion. Both engine call sites use exactly"%[^=]=%[^\n]", soStream::Scannow parses that shape by hand underPOLYPHASE_PLATFORM_ADDONwith avsscanffallback for any other format. A missingva_endwas also fixed.Small textures cooked to black on 3DS. tex3ds rejects textures below 8x8, and such a texture came through black on device where Wii and GameCube showed a flat tint.
CookTexturenow clamps up to an 8-pixel hardware minimum and warns, naming the Downsample Factor and Force High Quality as the fix.Build Allcrashed on the second profile. The queue's completion gate consulted onlyActionManager::IsBuildRunning(), but the build thread exits beforeFinalizeLocalBuildjoins it, and success auto-finalize runs inDrawBuildModal— which is called afterPackagingWindow::Drawin the same frame. In that one-frame window the next profile reassigned a still-joinablestd::thread, which terminates the process.IsAnyBuildInProgressnow also gates on the build thread being joinable, for both the local and Docker paths.The editor crashed at scene save or process teardown inside
Property::~Property. A priorLoadStreamhad left aPropertywith a corruptedmExtrapointer insidemNodeDefs, so destructing the vector faulted. New Windows-onlyTryClearNodeDefswrapsclear()in structured exception handling and falls back toLeakResetNodeDefsVector, which placement-news an empty vector over the old header — intentionally leaking the buffer rather than running destructors over corrupt memory.Scene::~Scenedeliberately logs nothing on that path, since it can fire during static teardown after the logger is gone.A truncated or miscooked
.octcaused gigabyte allocations or a delayed crash.Scene::LoadStreamfed garbageuint32_tcounts straight toresize(), splashing garbage through everyPropertyandSubSceneOverrideslot. Three guards at 65535 now clamp to zero and log, losing that node's overrides instead of taking the process down.A cyclic widget hierarchy crashed the editor every frame.
Renderer::DirtyAllWidgetscallsWidget::MarkDirtyon the world root each frame, and amChildrenlist containing itself or an ancestor made the recursion non-terminating. Added athread_localdepth counter capped at 512 plus an explicit self check, warning at most four times to avoid log spam.Property::GetSerializationSizeevaluatedmExtraunconditionally. Missing parentheses madeA + B + C + cond ? x : 0parse as(A+B+C+cond) ? x : 0, and the sum is always truthy, so the null check never applied. Wrapped so the conditional is an addend.CopyPropertyValuesproduced invalid Datums. Copying an allocated-but-emptymExtrayielded aDatumwithmType == Countthat later serialization choked on. Now requiresmExtra != nullptr && mExtra->IsValid(), matching the copy constructor,DeepCopyandWriteStream.Scene::Instantiatedereferenced a nullmExtraon legacy sub-scene overrides written beforeGatherNonDefaultPropertiesrecorded node paths. Added the null check alongside the node-datum-type test.Datum::PushBack(const std::string&)terminated the editor via_Xlen_string.GatherNonDefaultPropertieswalked a subscene-linked node whoseNameDatum pointed at astd::stringwith a garbage internal size field. Strings reporting a size above 1 MiB are now rejected and default-constructed, with a log line.A stale
Asset*surviving a project switch crashed on the firstPreRender.Quad::ResolveQuadTexturecalledasset->Is()on a dead vtable. It now checksAssetManager::IsAssetLiveand, on failure, clears the reference without decrementing a refcount on freed memory.Node::DisconnectSignalinserted an entry for every unknown signal name.mSignalMap[name].Disconnect(listener)default-constructs on lookup. Now checks the map is non-empty and the name is present first.Play in Fullscreen produced a
width x 0window from a maximized editor.SYS_GetWindowRectreads the restored rect on Windows whilemWindowWidth/mWindowHeighthold the maximized client size, so the computed chrome height went negative and collapsed the window. NewSYS_RestoreWindowis called before measuring, andEndPlayInEditorre-maximizes from a saved flag.Closing the window with X during fullscreen play-in-editor shut down the whole app.
WM_CLOSEunconditionally set the quit flag. Under#if EDITOR, when playing in editor outside a separate game window, it now requests an end to play-in-editor and returns. Related:EditorHotkeyMap::Destroy()moved afterEditorState::Shutdown(), because Shutdown can end play-in-editor, which pumps ImGui frames that reach the main menu bar's hotkey queries.Autocomplete dropdowns reopened immediately after a selection. The post-commit suppression latch compared against a
lastInputTextstatic shared by every picker drawing in the same frame, so it could not act as a per-id snapshot and cleared instantly. Now snapshots into a dedicatedcommittedInputText.Multiple asset pickers leaked keystrokes between each other.
DrawAutocompleteDropdownwas rewritten around a singleton active-dropdown id so exactly one is open at a time and the others short-circuit beforeBeginPopup, removing the global key-state mutation that caused it.ListView rows did not span the list and scrolling never activated. Covered by
ScrollContainer::SetContentWidgetand theCreateItemcross-axis fill default above.ListViewItemWidget::HandlePointerEventsusedthisafter free. User Lua routinely rebuilds the list from an item callback, freeing the item beforeEmitSignal,CallFunctionand the hover-state write ran. It now holds a weak self pointer and revalidates after everyOnItem*call.ClearandRebuildItemsalso checkIsDestroyed()before destroying.LineEdit margins changed with editor camera zoom. Two causes:
Widget::UpdateRect's margin arm addedmOffsetand subtractedmSizeraw, but those are authored pixels while the parent rect is in rendered space; andGetParentWidth/GetParentHeightreturned scaled letterbox pixels, so container widgets laid out differently in Game Preview than in Scene2D. Margins are now multiplied bymAbsoluteScaleand the accessors return the authored target dimensions.ArrayWidgetspacing and stretch layout were reworked as part of the flex rewrite;axisTotalis derived from the self rect divided by the axis scale rather than fromGetWidth/GetHeight, because the Game View letterbox shortcut inGetParentWidthreturns authored rather than scaled pixels.Fog stopped at the skybox.
material.mApplyFogbecame tri-state — 0 off, 1 distance fog, 2 sky fog — with a newApplyFogSkyinFog.glslthat fades by the view ray's elevation rather than by distance, since the sky sits beyond the fog far plane and distance fog would flood the whole dome.kHorizonFalloff = 0.25gives roughly a 14-degree fade band, full fog at and below the horizon clearing toward the zenith, so fogged terrain continues seamlessly into the sky. Sky detection is implicit — unlit, depth test disabled, negative sort priority — so no new per-material flag was needed.Skybox3D::Createand the legacyMesh3D::LoadStreamsky fixup now enable it.3DS fog was broken and its exponential path dead-coded. The LUT builder forced
data[0] = 0.0f, making the nearest depth read as fully fogged and corrupting the whole curve, and the delta half of the array was mis-indexed. The 3DS fog LUT is depth-indexed and stores per-sample clarity, with samples indata[0..127]and consecutive deltas indata[128..255].GFX_SetFogwas rewritten to that layout and now applies1 - clamp(powf(20, -fogAlpha), 0, 1)forFogDensityFunc::Exponential, matching the Vulkan approximation.ImGui asserted on first dock of an addon-registered window.
getRootDock()could return a container, whose empty label produced an empty ID in the tabbar'sInvisibleButton. The tab slot now descends through containers to the first real tab.Statically linking two addons collided on the plugin entry point.
PolyphasePlugin_GetDescisdllexportin the editor's hot-reload DLL build, whichNativeAddonManagerpicks up viaGetProcAddress. The shipped Standalone build links the addon's translation units without that define, so a plainPolyphasePlugin_GetDescresolved asdllimport— C2491 on MSVC — and two statically linked addons collided on the same symbol. A dual-entry pattern now emits a per-addon suffixed entry point for the static path. The suffix comes from a sanitizer that passes letters, digits and underscores through and rewrites everything else, notably the.and-in dotted addon ids, and it must stay aligned withActionManager::SanitizeAddonIdForSymbol, which emits the matching declaration intoGenerated/AddonPlugins.cpp.Addons failed to load with Windows error 126 after a build. The addon's post-build copy step drops companion DLLs — FFmpeg and similar load-time dependencies — beside the freshly linked addon DLL, but
MOD_Loadsearches only the loaded DLL's own directory viaLOAD_LIBRARY_SEARCH_DLL_LOAD_DIR. Anything sitting in the source directory but not in the shadow-copy directory vanished from the search path. The shadow copy now mirrors every regular file from the source directory, skipping the addon DLL and PDB already copied and any subdirectories. Load-side failures are also routed through a single helper soHasUnacknowledgedBuildFailurespicks them up and the build-failure modal opens with the verbatim error.Near-zero node scale crashed collision traversal.
btScaledBvhTriangleMeshShape::processAllTrianglesreciprocates every scale axis when mapping the query AABB, so a zero or near-zero axis produced infinite or NaN bounds; the quantized BVH then cast those tounsigned short— undefined behavior on MSVC — and traversal visited garbage leaves until the triangle callback faulted.StaticMesh3D,InstancedMesh3DandPaintManagernow pad any axis below1e-4to that magnitude, preserving sign so negative-scale mirroring still flips correctly.Changing a texture's Filter, Wrap or Mipmapped setting crashed with a device-lost error.
Texture::HandlePropChangerebuilds the image behind an unchangedTexture*, and every pointer-keyed ImGui descriptor cache — the inspector preview, voxel/terrain atlas, brush mask,TilePicker,TextureAtlasViewer,SpriteAnimationAtlasEditor— kept a descriptor bound to the freed view and sampler. The GPU faulted,ReadTimeQueryResultsgotVK_ERROR_DEVICE_LOST, and then asserted and froze the machine.Texture::GetResourceGeneration()(bumped in Create, Destroy and prop change) now keys every cache, with aDeviceWaitIdlebefore removal;ReadTimeQueryResultslogs and disables GPU profiling instead of asserting.Large textures drew black on 3DS with no diagnostic.
Tex3DS_TextureImportfails when VRAM (about 6 MB, with render targets taking about 2 MB) is full. Now retried on the linear heap, and freed withSYS_AlignedFreeto match the allocation.3DS Target Screen only worked on hardware. The filter ran only via separate worlds, so an editor-authored scene rendered an empty bottom screen, cameras inside bottom scenes reported screen 0, and lights from both screens lit both — the doubled diffuse and specular blew GX and C3D to white. Fixed by the resolution-order, camera-override and light-filter work above; the Scene panel and
SecondScreenPreviewnow use the same subtree lookup and honour0xFF; theTarget Screenrow is hidden in the inspector unlessIsSceneLinked(false).Arrays did not show up in the Properties panel.
Property::DeepCopyclearedmIsVectorfor internal-storage copies while keeping the externalmVectorpointer.mIsVectornow always copies andmVectoris null when storage is internal.Game Preview did not follow camera switches. It rendered a pinned
mCachedCameras[index]; the default is now the active camera —GetActiveCamera()in play-in-editor,GetMainCamera()otherwise.Spline broke the 3DS build and grew a phantom point.
glm::clamp(idx, 0, …)mixedintandint32_t, a template-deduction failure on the 3DS toolchain — literal cast added. And the starterpoint1created inCreate()reappeared on every scene reload; it moved toOnSpawnedInEditor().The editor crashed during long operations.
EditorProgress::Steppumps full frames, so a tab click mid-Scene::Instantiateopened and closed scenes and left the outer operation holding freedNode*andPropertypointers (an access violation inWeakPtr::IsValidviaDatum::DeepCopy). AddedmProgressPumping/IsInsideProgressPump()guards inOpenEditScene/CloseEditScene;CacheEditSceneLinkedPropsbuilds into a local vector;MapReplacedNodeDescendantsfixes Node-property refs into respawned sub-scenes; per-indexGetNode(nodeIdx)/SetNode(…, nodeIdx).A deleted asset directory left dangling pointers.
DeleteAssetDirnow scrubsmTabCurrentDir, history,mRevealAssetExpandDirsandmMouseHoveredAssetDir;DrawAssetsContextPopupreturnsitemDeletedsoDrawAssetDirTreeunwinds without touching the freed dir.SYS_MoveDirectorynow returnsboolon all platforms so a failed move does not update the in-memory tree.A REST spawn of an unknown node type crashed the editor.
EXE_SpawnNode(const char*)now checksNode::FindFactoryand returns null.Lua stack leaks.
ScriptFunc::Call / CallRnever popped the ref table, andCallLuaFuncleft the pcall error object on the stack. Both fixed.Network runt packets and unknown message types. Packets shorter than header + 1 are skipped; an unknown message type now breaks out of the packet instead of spinning forever.
AssetManagerdestructor null-checks the load thread.Ctrl+R re-fired during Reload All Scripts.
EditorProgress::Steppumps full editor frames while a blocking operation runs, but input state does not advance, soIsActionJustTriggered(Edit_ReloadScripts)stayed latched on the original press and the handler re-ran the C# transpile (a ~1.5 s dotnet spawn) plus directory refreshes between every script load. Fixed with a!GetEditorState()->mProgressPumpingguard.Play In Standalone on the Linux editor built a Windows package.
PlayTarget::Standalonealways calledBuildAndRunWithProfile(Platform::Windows); nowPlatform::Linuxunder#else. The Linux packaged binary extension was""and is now".elf".Create Project failed when parent directories were missing.
CreateNewProjectusedSYS_CreateDirectory(single level) for the project root,Assets,ScriptsandScenes; now usesCreateDirectoryRecursive.Addon shadow-copy startup noise.
StageShadowCopywalked every path prefix callingSYS_CreateDirectory, mkdir-ing the bare drive root and already-existing components, producing_mkdir error: Permission deniedplus a run ofFile existswarnings per addon at launch, and ignored failures so an uncreatable shadow directory surfaced later as a confusing copy error. Now usesCreateDirectoryRecursiveand returnsoutError = "could not create shadow dir ..."on failure.SceneNodeDef::mParentBonewasint8_t, silently corrupting bone attachments on rigs with more than 127 bones (fingers and twist bones). Widened toint32_tunder v39.Gizmo drags on bone-attached nodes exploded the scale. The gizmo writes through
SetTransform(worldMatrix), which back-solves position, scale and rotation every drag frame regardless of gizmo mode.Node3D::SetWorldScaledivided the extracted world scale by only the parent node's scale, never the bone term — and the bone matrix carriesmInvRootTransform, the FBX/glTF unit-conversion scale, 100× on Mixamo rigs — so each frame storedS_bone · S_localand the next frame extractedS_bone² · S_local. Every gizmo frame multiplied the scale by the bone factor, and becauseSetTransformcalls all three setters it fired on translate too. Now usesMaths::ExtractScale(GetParentTransform()), which is provably identical for non-attached nodes.Node3D::SetWorldRotationdouble-counted the parent rotation for bone-attached nodes: it multipliedparent->GetWorldRotationQuat()byGetBoneRotationQuat(), but that getter already bakes in the mesh node's world matrix. Invisible at identity rotation, wrong as soon as the character turned. Now derives fromGetParentTransform().Node3D::GetParentTransformreturned identity on a subclass. Theelse if (GetType() == SkeletalMesh3D::GetStaticType())was an exact-type compare with noelse, so anySkeletalMesh3Dsubclass — or a stale bone index — teleported the node to the world origin. Now seeds from the parent transform and usesAs<SkeletalMesh3D>().SkeletalMesh3D::GetBoneTransformreturned an uninitializedglm::mat4for an out-of-range index; now identity.ForwardSpec.fragignoredmIntensityfor directional and point lights; now multipliesmColor * mIntensitylikeForward.frag.ScriptUtils::RunScriptleaked Lua stack slots.LUA_MULTRETresults and error objects were never popped, so every reload sweep grew the main stack;lua_settopnow restores the snapshot.Hot-reloading a boot script such as
StartLuaPanda.luareopened the debugger socket and stole the Lua hook fromLuaDebugger; excluded viaIsReloadExcludedScript.Dolphin (Wii/GameCube) TCP connect always reported failure. IOS connects are asynchronous:
net_connectreturns-EINPROGRESSimmediately, and the oldrc == 0test read a healthy in-progress connect as a hard failure. Fixed withIsConnectInProgress(); note libogc'slwip/arch.hnumbering (EINPROGRESS=115) is not what the compiled call returns — on device it is newlib's-119, so<errno.h>is used.NET_SocketConnectis nowConnectAsyncplus a poll loop (NET_DOLPHIN_CONNECT_TIMEOUT_MS 15000) that restores blocking mode.Dolphin socket creation silently produced bogus handles.
net_socketreturns a negated errno, not -1, so an error like-22passed theNET_INVALID_SOCKETtest; newCreateDolphinSocket()normalizes to -1 and logs. The stream socket now usesIPPROTO_IP(IOS rejectsIPPROTO_TCP).<poll.h>is included only underPLATFORM_WIIbecause libogc2's GameCube tree has nopoll.h.NET_Initializenow logs ip/mask/gateway and warns when there is no default gateway, since off-subnet connects fail instantly rather than timing out.SYS_OpenDirectory(3DS/Dolphin): removed the "Could not open directory" error spam for optional directories probed at startup (Packages/,Scripts/, engine-content fallback) and theclosedir(nullptr)call, which is undefined behaviour on newlib.macOS: Cmd shortcuts dead in text fields, held keys typed once, keyboard lost after dialogs and fullscreen (beta.18) — three root causes, see Input fixes under macOS Platform.
UI-only scenes drew offset into a corner with black bands (beta.18). No
Camera3Dmeant no scene pass and no full-window paint; a transient fallback camera now fills the gap, see Engine and Rendering.UI clipped when Resolution Scale < 1.0 (beta.18). The UI render pass inherited the scene scissor; it sets its own now.
ContentPak::MountMemorycrashed on every call (beta.18).sMounts.push_back(std::move(mount))was followed byLogDebug("ContentPak: mounted '%s' from memory (…)", mount->mDebugName.c_str())through the moved-fromunique_ptr; it died insidestd::string's SSO check and looked like string corruption. The log moved above thepush_back.PS2 hardware:
not a valid pak, scrambled index,_request_endnull-packet crash, audio glitching during streaming reads (beta.18) — shared SIF RPC buffer, unaligned cdvdman reads, recycledFILEbuffer; see Content Pak Mount Stack and PS2 Hardware I/O.Xbox 360 faulted silently on the first widget (beta.18) —
thread_localwith no TLS runtime;POLYPHASE_THREAD_LOCALandPOLYPHASE_NO_TLS.Runtime-generated textures sampled black on Adreno / Mali (beta.18) —
PREINITIALIZEDinitial layout with a host-write barrier under no HOST stage, plus an unfenced one-shot upload; see Engine and Rendering.Android
Log To Fileproduced no file (beta.18) — relativefopenin a NativeActivity; nowinternalDataPath.Every native addon's Android
nativePerPlatformblock was ignored (beta.18) —"android"vs the case-sensitive"Android"key; editor rebuild required.The Android build failed on
VK_INSTANCE_CREATE_ENUMERATE_PORTABILITY_BIT_KHR(beta.17) — not declared by the NDK'svulkan_core.h; the block is#if PLATFORM_MAC.Headless Linux (
-serve, SSH, CI) aborted before it could serve (beta.17) —ImGui::GetIO()/GetStyle()asserts with no context inEditorImguiDraw,EditorState::UpdateandThemeModule;xcb_poll_for_eventon a null connection inSYS_Update;snd_pcm_avail(nullptr)inAUD_Update; the render loop against a nullVulkanContextinEngine::Update;EditorProgresspumping a renderer that did not exist. All guarded onIsHeadless()/ null device.Headless imports stranded (beta.17) — the texture and WAV fixup modals queued rows nobody could answer, leaving
mPixelsempty and the save skipped; they now auto-pad / auto-resample and warn.Saving into a new nested asset folder was a silent no-op (beta.18) — single-level
CreateDir()inSaveAsset; ancestors are created root-to-leaf.Importing a mesh with no normals segfaulted (beta.18) —
mNormalsnull on a bare.obj;aiProcess_GenSmoothNormalsplus null guards.The
Loggingtoggle and a project'sConfig.inilogging flag had no effect until restart (beta.18) —EnableLogis now called on change and inLoadProject.A scene file duplicated outside the editor opened with the old name in its tab (beta.18) — the serialized
Namewon over the stub name;LoadAssetreconciles them.A skeletal mesh loaded from disk reported the unit AABB (beta.17) —
LoadStreamrestored the sphere, notmAABB;Create()callsComputeBounds().LNK2019: Sha256::HashHexin addon builds, then undefinedSha256::HashHexon 3DS / GameCube / Wii (beta.17) — MSVC dropped the unreferenced object; forced linkage; console Makefiles gainedSource/Engine/Utils.compile.bathung packaging on any shader failure (beta.17) —pauseat the end of a script the editor streams; andcdwithout/dnever reached the script from another drive. A missingglslcno longer fails a package whenEngine/Shaders/GLSL/binis complete.Installers/package_windows_sdk.pycrashed withNameError: name 'os' is not defined(beta.17) at the Vulkan-header staging step on every Windows SDK release run;import osadded.A GCC-built editor emitted a corrupt Mac
PkgInfo. The generated shell lineprintf 'APPL????' > "$APP/Contents/PkgInfo"contains the??'trigraph, which GCC rewrites to^, so the emitted packaging script wrote garbage; now'APPL?\?\?\?', same output bytes.BannerGltfExporter.hgained#include <cstdint>(libstdc++ does not provide the fixed-width types transitively where MSVC does), and a*/inside aMacBundlePackager.hdoc comment (MacOS/Addons/*.dylib) that terminated the comment early was reworded.SYS_Logon GameCube / Wii re-expanded%in log lines (beta.18) — the formatted buffer was passed as the format string; and the libogc v1 / libogc2SYS_Report/SYS_Reportvsplit was backwards relative to its own comment.Addon REST routes silently truncated responses over 4 KB (beta.18) — 256 KB heap buffer.
A disc-image build profile could be saved with a layout that does not boot (beta.18) —
polyphase.discContentlocks Embedded off / Static Content on / Content Pak on.
Deprecated
The C++ project type. The project-type radio buttons are commented out — both
LuaandC++in Create Project, andC++in Clone From Git — so new projects are Lua-only. The underlyingmProjectTypeandmCloneProjectTypefields and their code paths remain for now. The recent-projects button was also relabelledBrowse...toOpen Project.SYS_CopyFilereturningvoid. The signature is nowboolon every platform backend and callers are expected to check it. Out-of-tree platform backends must update.Renderer.GetScreenIndex()andRenderer.GetActiveScreenResolution()removed from Lua. UseNode:GetTargetScreen()/Node:GetScreenResolution()on the node, orRenderer.GetScreenResolution(index)with an explicit index.SYS_MoveDirectoryreturningvoid. Nowboolon every platform backend, alongsideSYS_CopyFile.ContentPak::MountreturnsMountHandle(wasbool);Unmount(MountHandle)(was no-arg);FindEntry(path, MountHandle&, uint32_t&, uint32_t&)(was three-arg);GetPakPath(MountHandle)(was no-arg) (beta.17). Addon callers must update;UnmountAll()replaces the old unconditional unmount, andIsMounted()now means "at least one mount".POLYPHASE_NO_TLS(beta.18) must be defined by any build target for a platform with no TLS runtime (Xbox 360) so Bullet'sbtThreads.cpp/btQuickprof.cppdropthread_local; engine code should declare statics reachable on such targets withPOLYPHASE_THREAD_LOCAL, notthread_local.Image::Update(const void* srcData, bool waitForCompletion = false)(beta.18) — defaulted, source-compatible; call sites that upload once and sample immediately should passtrue.
Licence
- The repository licence is now MIT-PoU — the MIT License with a Proof-of-Usage Condition (PR #50,
ebellumat/proof-of-usage-licence). Every MIT permission is unchanged. The one added condition is addressed to operators of automated systems (training corpora, RAG indexes, agents browsing at inference time, code assistants, evaluation harnesses): record the usage as a row inPROOF_OF_USAGE.mdand print a matching provenance hash —SHA-256("SystemName:OperatorName:ISODate:TargetRepositoryURL")— in the product's credits. No payment, no takedown, no restriction on what may be built. New files:AGENTS.md,PROOF_OF_USAGE.md,proof-of-usage.json(PoU/1.0);LICENSEandREADME.mdupdated. For humans reading, forking, learning from or shipping with the engine, nothing changes.
Chore
Documentation
New:
Documentation/Development/SetupEnvironment/Mac.md,Documentation/Development/Platforms/Mac/Overview.mdandPackaging.md,Documentation/Development/OcclusionCulling.md(concepts, workflow, bake internals, memory table with a worked example, cell limits, C++ / Lua / REST API), all inmkdocs.yml. Updated:PackagingFlow.md(Mac and CIA rows),Platforms/3DS/Overview.md(SMDH and installable CIA),BuildProfiles.md(3DS targets table),Compiling.mdandTerminal.md(Mac steps),Windows.mdandLinux.md(DEVKIT*verification by full path, CIA tools),CustomBuildTarget.mdandNativeAddon.md(seven built-in targets,MacBundlePackager.cppas the reference PostPackage,Mac/PSPinnativePerPlatform),CICD.md(header staging, the not-yet-automated macOS.dmg),README.md. Lua docs:Lua/Systems/Renderer.md,Lua/Nodes/3D/Primitive3D.md,Lua/Misc/World.md..llm/Platforms.mdand.llm/Spec.mdadd macOS (and note the Makefiles are canonical over CMake on desktop);.llm/Graphics.mdinserts step 3bOcclusionCull;.llm/Addons.mddocumentsResolveVulkanIncludeDir(),RunBuildPreflight(),ClassifyBuildFailure(), the "Native Addon Problem" modal,AddonDependencyResolver, cascading removal and update detection. Skills:polyphase-addongains troubleshooting rows forC1083 vulkan/vulkan.hand missing dependencies,polyphase-buildtargetcounts seven targets,polyphase-controllerdocuments the occlusion REST routes.Documentation/Development/SkeletalAnimation.md— full skeletal animation guide covering multi-section meshes, clip extraction and import, avatars and both retarget tiers, including the Tier-2 math and an honest limitations list.Documentation/Development/BoneMask.md— bone masks, the eight animation slots, layer modes and the Bone Mask Editor.Documentation/Development/StaticContent.md— Static Content and Content Pak: container and archive layouts, the decode chokepoints, cook ordering, the Embedded interaction, and explicit "obfuscation, not DRM" messaging guidance.Documentation/Development/StaticContent-BuildTargetBrief.md— what Static Content and Content Pak mean for build-target addon maintainers, theDrawProfileOptionsopt-in recipe, and a target classification table. Note thatbasePlatformis a cook-compatibility anchor, not a statement about the runtime backend.Documentation/Development/PackagingFlow.mdgains steps[8b] Static Content / Content Pakand[8c] Romfs copy (3DS).Documentation/Development/CustomBuildTarget.mdandDocumentation/index.mdupdated;.llm/AssetSystem.mdand.llm/Spec.mdrefreshed;.llm/SkeletalAnimation.mdand.llm/BoneMask.mdadded.Documentation/Lua/Assets/SkeletalAnimationAsset.mdandDocumentation/Lua/Assets/HumanoidAvatarAsset.mdadded..claude/skills/polyphase-buildtarget/SKILL.mdexpanded..github/workflows/docs.ymlaction versions bumped to clear Node 20 deprecation warnings.3DS
Documentation/Development/Platforms/3DS/Screens.md(resolution order, Lua notes);Documentation/Development/SetupEnvironment/Linux.md(RPM/AppImage host tools);Documentation/Development/NativeAddon.md(exported surface); LuaWorld.md/Node.md/Renderer.md;mkdocs.ymladds Bone Mask and Static Content pages.Documentation/Info/CSharpScripting.md— C# scripting user guide.Tools/PolyphaseSharp/README.md— architecture.Documentation/Lua/Networking/WebSocket.md— full Lua reference, limits table, GodotWebSocketPeercorrespondence table, "keep a reference onself" warning.Documentation/Development/Networking/WebSocket.md— platform matrix, file map, seam rationale,NET_additions, downgrade opt-in table, testing steps.Tools/ws-echo/README.md.Documentation/Development/BoneSockets.md— quick start (attach, place, promote to socket, reuse), direct authoring, draw/sheathe scripting, notify-driven swaps, effects at a socket without attaching, gotchas..llm/BoneSockets.md— internals, the three transform bug fixes, notify semantics.Documentation/Lua/Nodes/3D/Node3D.mdandSkeletalMesh3D.mdgained the socket, time and notify entries;Documentation/Lua/Nodes/3D/SpotLight3D.mdadded.BoneMask.mdandSkeletalAnimation.mdcross-link.mkdocs.yml,Documentation/Lua/README.md,.llm/Spec.md(asset version 39) updated.Documentation/Development/SetupEnvironment/Windows.md,Linux.mdandDocumentation/index.mdrewritten for the devkitPro steps:wii-dev/3ds-devare the meta-packages that pull in the actual compilers (devkitPPC'spowerpc-eabi-g++, devkitARM'sarm-none-eabi-g++) — thelibogc2packages are only the libraries, and GameCube compiles with devkitPPC too; thepacman.confentry is called out as file content, not commands, with both mirrorServerlines belonging in the file; each command is now its own fenced block; verification commands added.(beta.17 / beta.18)
Documentation/Info/FAQ.md(+159 lines):## No Camera3D in the sceneunder the black-screen section, and a new# Androidsection —Seeing logs from an Android build,Log To File produces no file on Android,Parts of the UI are missing — UI offset into a corner, or large black bands,What Scissor does, and when to turn it off,A runtime-generated texture (photo snapshot, video frame, procedural image) renders black on Android but is fine on Windows,A native addon behaves as if its platform code isn't there on Android,Which changes need the editor rebuilt before packaging for Android?,An Android feature needs a runtime permission (camera, microphone, location).Documentation/Development/SetupEnvironment/Mac.mdnotes thatbuild-macinstalls the same SDK through.github/actions/install-vulkan-sdk-mac, that bumping the version means the doc, the action default and theactions/cachekey, and that uploading the unmodifiedvulkansdk-macos-<ver>.zipto avulkan-sdk-<ver>GitHub Release makes CI independent of LunarG's CDN retention..llm/Editor.mdgains theCharacterCreator/row. Skills:polyphase/SKILL.mdrestructured (fallback-camera section,Scissorreference);polyphase-widgetgains scissor and fallback-camera guidance plus a Common Gotchas section (NewTransientAsset<Texture>(),SetRetainPixelsbeforeCreate(),SetAnchorModenot sizing a widget withoutSetFullScreen/SetMargins, alpha-0 container cascading, LuaCreate()vsStart()ordering,Signal::Emitnot prepending the emitter,SetScriptFilepath doubling);polyphase-addongains the case-sensitivenativePerPlatform.Androidtroubleshooting row and the Android runtime-permission pattern;polyphase-buildtargetgains the explicit-SOURCESMakefile rule with a Python verification snippet.
Refactors and Maintenance
BuildDependencyWindow.cpprewritten whole-file (CRLF churn) alongside the new checks;ProfilingWindow,BuildProfile::GetPlatformOutputExtension/PlatformUsesShaderFilesandBuiltInBuildTargets::kMacIdextended for Mac;JsonSettingsskips the legacyOctaveEditormigration on Mac;EditorHotkeyMappresets under~/Library/Application Support/PolyphaseEditor/HotkeyPresets.Engine.vcxprojlists the new Mac sources (.mmasNoneitems) and the occlusion, CIA and Mac packager files.ActionManagerextended for mesh material and texture import, animation extraction and import, retargeting, the build-writability probe, content obfuscation and packing, and node-type replacement (roughly 1.6k LOC net).EditorImguireorganized for the categorized Create Asset menu, the Other-node categories, the quick-spawn toolbars, the Script Panel context menu and its three modals, the game view border, and the autocomplete dropdown rewrite (roughly 1.3k LOC net).SkeletalMesh3d.cpprestructured around slot-based blending, masking, additive layering and binary-search keyframe lookup.AudioManagergains the streaming source pool, the platform-conditional chunk sizing and the optional I/O thread.PackagingWindowgains the Static Content and Content Pak checkboxes and the Build All queue.Widgetgains fill anchors, the parent-rect override, the Game Preview letterbox path and the recursion guard;ArrayWidgetrewritten around two-pass flex layout.FileWatcherrewritten as a portable polling watcher.Several
*Platform_*Types.hregenerations underStandalone/Generated/;AddonInject.cmakepaths and the Android demo namespace retargeted to a file-browser sample..gitignoreand.dockerignorehousekeeping.Tools/generate_lua_stubs.pyacceptsNs::FuncinREGISTER_TABLE_FUNC, tracks per-table registrations and emits separate stubs for multi-table binders (Http→HttpResponseand similar).Engine.vcxproj/ filters andMakefile_Linuxadd the MemorySnapshot, Packaging and EditorImageCache sources.Standalone/Generated/regenerated (~280k lines churn, generated code).Tools/PolyphaseSharp/External/CSharp.luavendored (~40k lines).Tools/PolyphaseSharp/Tests/committed with itswork/outputs and a prebuiltlua.exe.Engine/Assets/Scenes/Blueprints/SC_Demo_*Pawn.oct,Levels/SC_Demo_*.octand demo materials resaved at asset version 39..claude/settings.local.jsonand.vscode/settings.jsonhousekeeping.(beta.17 / beta.18)
POLYPHASE_VERSION_STRING6.2.0-beta.16→beta.17(21997de92and9820acc85are byte-identical bumps on two parallel branches, neither an ancestor of the other) →beta.18(insidecd0d422a9).Standalone/Generated/Generated/EmbeddedAssets.cpp/EmbeddedScripts.cppregenerated (~283k lines); nineEngine/AssetsM_Demo_*/M_SkyboxDay.octfiles re-saved with no size change;Standalone/Generated/PolyphasePlatform_{Audio,Input,Network,System}Types.hflipped from the PSP target to the PS2 target and a four-linepolyphase_xbox360_build.shcommitted — regenerated-on-build files carrying absolute developer-machine paths.Standalone/Androidtemplate identity (build.gradlenamespace / applicationId,strings.xmlapp name) changed twice by local test projects and reverted tocom.you.polyphaseb/Polypohase testin7cd338f53(the typo is in the source);AddonInject.cmakeregenerated empty.EditorImgui.cpp,Engine.cppandSystem_Linux.cpprewritten wholesale by CRLF/LF normalisation in3612e567f(65 real added lines across six files)..github/workflows/release.ymlcreate-releaseneeds/ download pattern / asset list extended for Mac.Engine.vcxproj+ filters,Makefile_LinuxandMakefile_MaclistSource/Editor/CharacterCreator.