This file tracks all functions extracted from Apus.Common into new modules.
Use it as the primary reference when updating old code.
Apus.Engine.UI:FindControl,FindControlAt,FindAnyControlAtare gone; useFindElement,FindElementAt,FindAnyElementAt.Apus.Geom2D:IdentMatrix2,IdentMatrix32are gone; useIdentMat2d,IdentMat32d.Apus.Engine.API:TGraphicsAPI.gaOpenGL(fixed-function GL) is gone; the enum is(gaAuto, gaOpenGL2).Apus.Images: theConvertLine(sour,dest,sf,df,palette,palFormat,count)overload is gone; useConvertLine(sour,dest,sf,df,count,@palette,palFormat)(palette and format may be omitted when there is no palette).Apus.HashMaps:TSimpleHashASandTSimpleHash8are gone; useTHashMap<int64>. The other legacy hashes (TStrHash,TSimpleHashS,TVarHash) stay for now.
Apus.Engine.ImgLoadQueuelaunches its loading and unpacking workers throughApus.Threads.Thread.Startand tracks them asIThread; theClasses.TThreadsubclasses are gone. The public API (StartLoadingThreads,QueueFileLoad,GetImageFromQueue) is unchanged.- The loading worker now checks for termination while waiting for an empty queue, so shutdown no longer hangs when threads were started but nothing was queued.
liffMipMapsno longer impliesaiPow2: an image loaded with mip-maps keeps its own size (NPOT mip chains are baseline in GL 3.3 / GLES 3.0),u2/v2are 1. Padding happens only with an explicitliffPow2/aiPow2.- POT-padded textures (
aiPow2with an NPOT image) drew garbage (horizontal stripes): the CPU copy of a level is packed by the image width, the upload read it with the storage width. Reading such a texture back (LockafterUpload/UploadPart/ClearPart) failed or overran the buffer. Both fixed. mipmapsof an auto-mip-mapped texture is floor(log2) of the larger side (the last level of an NPOT chain), still capped byMAX_LEVEL; a fullLockof a level of an odd size covers exactly that level.
- Robot API commands renamed:
mouse.mode/mouse.reset/mouse.state/mouse.wait->input.mode/input.reset/input.state/input.wait. The virtual mode covers the mouse and the keyboard together;mouse.move/down/up/click/wheelkeep their names. - New commands
key.down,key.up,key.tap(MODS,HOLDin ms),key.type(text asKBD\UNICHAR). Keys byKEY(TKeyname) orSCAN(Scan.XXX); the answers gainkeysandfocus/focusClass. In the virtual mode the physical keyboard is ignored too. Seerobot_api_protocol.md, "Virtual input". Apus.Engine.Window:TVirtualMouse*->TVirtualInput*(vmo*->vio*,vmt*->vit*),window.virtualMouse->window.virtualInput,FindVirtualMouse->FindVirtualInput,FrameMouseInput->FrameInput.- New
TKey.FromName(name)(Apus.Engine.Keys):TKey.Nameor an identifier alias (D1,Semicolon...), case-insensitive. window.shiftStateis updated byTWindow.UpdateShiftStateonce per frame for every window (extra windows used to keep it at 0); in the virtual mode it comes from the virtual keys held.
- The scancode of
KBD\KEYDOWN/KBD\KEYUP/KBD\CHAR/KBD\UNICHAR, ofTGameScene.onKeyDown/onKeyUp/DispatchKeyand ofIsKeyDown/IsKeyPressed/IsKeyReleasedis the USB HID usage id (=SDL_Scancode), the position of the key, the same on every platform and layout. It used to be the platform's own code: Windows scan set 1 (extended keysor $80) or, under SDL, a mix of SDL codes and a few set 1 values.- New namespace
Scan(record with constants,Apus.Engine.Keys, re-exported byApus.Engine.API):Scan.A,Scan.D1,Scan.Up,Scan.LShift,Scan.Num5... named after the US layout;Scan.Count(512) - every scancode is below.Scan.ToKey(code)/Scan.FromKey(key)-TKeyat a position on the US layout and back;Scan.Name(code). - Numeric scancodes in game code must be replaced:
IsKeyPressed(2)(set 1 key1) ->IsKeyPressed(Scan.D1),$C8(Up) ->Scan.Up,$1C(Enter) ->Scan.Enter. window.keyStateisarray[0..Scan.Count-1], the bits changed: bit 0 - down now, bit 1- went down in the frame, bit 2 - went up in the frame. Change it only through
TWindow.KeyTransition.IsKeyPressed/IsKeyReleasednow see a press shorter than a frame (both true); an auto-repeat is not a new press. A scancode out of range gives false.
- went down in the frame, bit 2 - went up in the frame. Change it only through
- A release of a key that is not down is dropped (
TGame.KeyPressed): the receivers get down/up pairs. - New
TWindow.PlatformReleaseKeys: on focus loss (WindowsWM_ACTIVATE, SDL focus lost/hidden/minimized) every held key getsKBD\KEYUP- a held key no longer sticks. - Scene key buffer packing (
WriteKey):keyCode|scancode<<8|pressed<<24. GetKeyEventScanCode(tag)reads the scancode where the platforms put it (tag shr 16).
- New namespace
TKey: newPause,LWin,RWin,Menu,LShift,RShift,LCtrl,RCtrl,LAlt,RAlt(Windows already sent these codes).TKey.FromSDLremoved. Under SDL theTKeyof a key event comes from its position except letters and digits (which follow the layout, as Windows VK codes do): punctuation, keypad and modifiers no longer get wrong codes ('used to arrive asRight,Num1asY).ISystemPlatform.MapScanCodeToVirtualKeyremoved (unused).
-
Removed (nothing read them):
TWindow.textLink,textLinkRect(the current text link istxt.Link/txt.LinkRectinApus.Engine.TextDraw),frameStartSec,frameDeltaSec(useframeStartUs/frameDeltaUsor the*Msones),paused,mouseMovedTime. -
Private now:
dRT,dRTdepth(the depth texture iswindow.GetDepthBufferTex),runtimeLock(useLockState/UnlockState). -
window.FPS/smoothFPS->window.timings.fps/smoothFps. -
Frame log:
window.FLog(st)->window.frameLog.Add(st)(game.FLogstays);window.frameLog/prevFrameLog(strings) ->window.frameLog.current/previous. -
Frame capture moved into
TFrameCapture(window.capture):window.RequestScreenshot->capture.RequestScreenshot,window.RequestFrameCapture->capture.RequestFrame,window.CaptureFrame->capture.Capture(window). They don't take the window lock: usegame.RequestScreenshot/RequestFrameCapture, which do.TGame.CaptureFrameremoved. -
Mouse helpers:
MouseInRect,MouseIsNear,MouseWasInRectremoved fromTWindow,TGameBaseandTGame. New namespaceMouse(record with class functions, declared inApus.Engine.Window, re-exported byApus.Engine.API) reads the pointer of the current thread's window:Mouse.Pos(TVec2),X,Y,InRect(TRect/TRect2),InCircle(x,y,radius),Down(buttons=mbLeft)(any of the buttons is held),OverUI,Simulated(the virtual mouse is in control). Position queries are false while the pointer is outside the canvas.MouseInRect(x,y,width,height)has no replacement overload: it was easy to call with corners. UseMouse.InRect(Rect2(x,y,x+width,y+height)).MouseIsNear->Mouse.InCircle.MouseWasInRect->Mouse.WasInRect(see the input frame below; the old one matched the current position outsideonMouseMove).
-
mbLeft/mbRight/mbMiddleare declared inApus.Engine.Types(still available fromApus.Engine.API). -
Input frame: the input is published once per pass of
TWindow.OnFrame, at its start (all paths: the main loop,WM_PAINT, extra windows). New frame queriesMouse.WasPressed,WasReleased(any of the buttons went down/up since the previous frame; a click shorter than a frame gives both) andWasInRect(TRect/TRect2)(the pointer at the previous frame); newMouse.Outside(the pointer is not over the canvas, also while a drag clamps it to the edge; position queries are false then).mouseButtonsfollows the button events at once (physical, virtual, touch); it used to come from a poll before the message pump - a frame late, and a click between two polls never reached it. The poll is gone (TWindow.SetPolledMouseButtonsremoved).window.oldMouseButtonsremoved: it changed only on a button change, so the usualmouseButtons and not oldMouseButtonswas true in every frame the button was held. UseMouse.WasPressed/WasReleased.TWindow.MapPointerToCanvasremoved: platforms report the pointer withSetPointer(clientPos)/SetPointerOutside(same mapping and clamping).PlatformMouseButton(btn,pressed,sample=true): touch passessample=false. Touch no longer sendsMouse\BtnDown\Left/Mouse\BtnUp\Left: it sendsMOUSE\BTNDOWN/MOUSE\BTNUPwith tag 1, like the mouse.- The keyboard frame (
IsKeyPressed/IsKeyReleased) moved to the same boundary and now works in every window (it was shifted by the main loop for the main window only). - A nested
OnFrame(a modal message pump inside a handler) is refused. ISystemPlatform.GetMouseButtonsremoved (nothing polls the buttons now).- New
TWindow.PlatformCancelGesture: the platform lost the pointer - a physical gesture ends without a click, the buttons are released. Windows calls it on deactivation,WM_CANCELMODEand a capture taken by another window, and releases its capture only after the last button release is delivered; SDL calls it on focus loss, hide, minimize.
-
Scene notifications:
TWindow.NotifyScenesMouseMove(nothing called it),NotifyScenesMouseBtn,NotifyScenesMouseWheel(one-line forwards to the UI dispatcher) removed;NotifyScenesResizeis private.TGameScene.ModeChangedandTWindow.NotifyScenesModeChangedremoved: nothing overrode the hook, and a display mode change already reaches the scenes asonResize(andENGINE\SURFACECHANGED). -
TGameBaseproxies to the current window removed:game.ClientToCanvas/CanvasToClient/GetDepthBufferTex/Minimize-> the same methods ofwindow;game.CanvasSize->window.surface.canvasSize. Scene methods (AddScene,GetScene,TopmostVisibleScene...) stay: they work on the main window.
IResourceManager.MakeOnline(img,stage)->MakeOnline(img): uploads pending data and binds nothing. Binding for drawing isIShader.UseTexture(tex,stage).IShader.UseTexturetakes stages 0..15. Units 16..30 belong to the engine (shadow map 16, normal map 20), unit 31 is the scratch unit for uploads and readback (Apus.Engine.ResManGLconstantsTEX_*). The normal map moved from unit 3, the shadow map from unit 9.- New
TGLContextRequest.baseline/ command line-GLBASELINE: no GL features above 3.3 / ES 3.0 (TGLFeaturesinApus.Engine.ResManGL: DSA, clear texture), to check the baseline paths on a newer GPU. TheEXT_direct_state_accesspath is gone.
IResourceManager.Clone(img) -> img.Clone (TTexture method; ClonePart for a part).
The manager clone shared the GL texture name, so freeing it deleted the parent's texture.
IResourceManager.Copy was a stub that always raised; removed with no replacement.
TWindow.name was the window title (any Unicode text; two extra windows with the same title
failed with "Duplicate object name"). Now it is an ASCII identifier (letters, digits, _,
-, .; checked by ASSERT), the title stays a separate caption:
ISystemPlatform.CreateWindow(title)->CreateWindow(name,title)game.AddWindow(settings)/AddWindow(title,w,h)take an optional lastname; empty -Window1,Window2...- the main window is named
Main(was its title); platform window constructors have no'MainWnd'default - Robot API
WINDOWparameter ofmouse.*addresses windows by this name
Base/Apus.Clipboard is replaced by Apus.Engine.Clipboard: the clipboard is a platform
service (on Linux/Wayland it needs the window connection), and all callers are engine modules.
The functions became the Clipboard record namespace:
CopyStrToClipboard(UTF8String|WideString)->Clipboard.SetText(String8)(written asCF_UNICODETEXT; the old UTF-8 overload wrote the bytes as ANSICF_TEXT)PasteStrFromClipboard,PasteStrFromClipboardW->Clipboard.GetText:String8(no 1250-character limit of the old fixed buffer)PutImageToClipboard(TObject)->Clipboard.SetImage(TRawImage)- new:
Clipboard.HasText,Clipboard.HasImage,Clipboard.GetImage:TBitmapImage(PNG, then 24/32 bppCF_DIB; caller frees)
Windows uses WinAPI with either platform layer. Other platforms: text via SDL, no images.
- TImageFileType now includes ifWebP. CheckImageFormat recognizes static RIFF/WEBP VP8, VP8L, and VP8X headers and reports dimensions and ARGB output. Animated WebP is rejected.
- LoadWebP(data,image) decodes a static WebP to a new or caller-provided image. Define WEBP to link libwebpdecoder; without it the API raises NotImplemented. The engine file loader and preloader accept .webp. Save/encode is not added. An opt-in build must ship the decoder under the binding name: libwebpdecoder.dll on Windows, libapuswebpdecoder.so on Linux/Android, or libwebpdecoder.dylib on macOS. Windows x64 and Linux x64 binaries are built from pinned libwebp source and bundled in bin64/ and redist/linux/. Android arm64-v8a is bundled in redist/android/; macOS and iOS are deferred. Extensionless file probing includes .webp only in WEBP builds.
- The shared FPC image reader now respects the target pixel format and row pitch, including Mono8 and A8 targets, for PNG and JPEG. Previously it wrote four bytes per pixel even into one-byte grayscale PNG targets.
- PNG decoder measurements, limits, and the compile-time selection decision are documented in PNG_DECODERS.md. LODEPNG behavior is unchanged.
TTextEffectLayer moved from Apus.Engine.API to the new module Apus.Engine.TextEffects
(R-33): add it to uses. Layers are passed explicitly to DrawTextFX; there is no
textEffects[] state in the text drawer (Engine 2 WriteEx has no direct counterpart).
The fields changed, there is no mechanical mapping - re-tune the values by eye:
| Engine 2 field | Engine 5 |
|---|---|
enabled |
removed: pass only the layers you want |
blur (soft 3x3), fastblurX/Y (box), power |
blur (gaussian, visible radius) + spread (round dilation) |
emboss, embossX/Y |
removed |
color, dx, dy |
unchanged |
Typical layers: TTextEffectLayer.Glow(color,blur,spread), .Outline(color,width),
.Shadow(color,dx,dy,blur).
Loading the same image file twice used to create a second texture and, since the name
registry became strict, failed with Duplicate object name. The loader now shares:
LoadImage/LoadImageFromFilelook the file up in the source registry first. A hit on an immutable texture with the same storage settings returns the same object and increments its reference counter;FreeImagereleases one holder. A mutable or differently configured texture is not shared: a private copy is loaded and a warning logged.- Loaded textures are immutable by default (
MakeImmutable); passliffAllowChangeto get a mutable private copy. Writing to a loaded texture without the flag raises, as it already did throughtfNoWrite. TTexture.AddReftakes one more strong reference to an existing texture (returnsself, nil-safe); release it withFreeImage.TTexture.FindByNamestays a weak lookup: the creator keeps a named texture alive. Do not recreate a named texture while strong references to the previous one exist (unique names).TTexture.SourceKey(ref)is the canonical key of a file reference: separators fixed,defaultImagesDir/exe dir stripped, extension and case kept; lookup is case-insensitive. A loaded texture is indexed under its file key (src, with the extension of the file actually read) and its image key (without extension).TTexture.FindByFile(ref): a reference with extension finds that file, one without finds the image loaded from any of its files.foo.tgaandfoo.ddsare different textures.defaultImagesDirmoved fromApus.Engine.ImageToolstoApus.Engine.Resources.- File textures are named
_<file key>(non-unique label). Code that looked them up by name must useFindByFileor give the texture a name explicitly. CropImage(image,x1,y1,x2,y2)is now a function returning aClonePartview; the source handle is not modified. Free the result like any texture.- Clones (
Clone/ClonePart, atlas parts) get_-prefixed non-unique names.
Design and rules: manual/ch21_resource_system.md.
tfReadOnly, abReadOnly and bfReadOnly were easy to confuse with tfNoWrite, which
only forbids CPU writes through Lock (render targets carry it while the GPU keeps
changing them). The flags set by MakeImmutable mean "content is frozen" and are now
named after the method:
| old | new | where |
|---|---|---|
tfReadOnly |
tfImmutable |
texture caps (Apus.Engine.Resources) |
abReadOnly |
abImmutable |
buffer allocation flags (Apus.Engine.Resources, re-exported by Apus.Engine.API) |
bfReadOnly |
bfImmutable |
buffer caps (Apus.Engine.Resources) |
tfNoWrite is unchanged. Design context: manual/ch21_resource_system.md.
Content keys. Drawers resolve the text part of a widget through one helper
(ResolveTextStyle in Apus.Engine.DefaultStyle), so these keys now work on labels,
buttons, checkboxes, containers, windows and combo boxes:
| key | meaning |
|---|---|
text-align |
left/center/right/justify; overrides TUILabel.align |
text-shadow |
<color> [dx dy] or none; dx,dy in logical units (1 1 by default) |
text-decoration |
underline or none |
text-offset-x/y |
caption shift in logical units, blended per state |
- The button's hardcoded 1px caption nudge while pressed became the default of
:pressed { text-offset-y }— a style can change or remove it. Being a logical unit, it is now 2 real pixels at scale 2 (it used to stay 1 pixel). color(content color), text offsets and the caption color blend through the state tweenings; align, decoration and shadow switch instantly.ParseStyleNumberis exported fromApus.Engine.Style(compound values liketext-shadoware parsed by the drawer).
State blocks inside named styles now apply (defect fix). A :hover { ... } block
written inside Styles['name'] was silently ignored: state lookups only ever saw the
element's own block, and never expanded &tokens. Both are fixed, so
Styles['button']:='fill:&control; :hover { fill:&accent; }' works as written.
- New cascade order (states beat plain values at every level):
local
:state>@ref:state> local plain >@refplain. A named style therefore keeps its hover/pressed behaviour even for an element that overrides the same key without a state; to keep local colors in every state, declare local:stateblocks (they win). - New API:
ResolveBlockStateAttr/Color/Number(Style) andTUIElement.GetStateStyleValue/Color/Number— use these instead ofelement.style.GetStateColor/GetStateNumber, which see neither refs nor tokens. TStyleBlock.GetStatesValue(states,key)evaluates a comma-separated state list.- Visible effect: widgets whose
@refdeclares states (StyleDemo's@demo-button, any theme-provided style) start animating between state colors.
A widget box can now be a texture: background-image: tex:<name> / file:<name>,
with background-tint, background-size: auto|stretch and background-offset-x/y.
The common box path (DrawCommonStyle in Apus.Engine.DefaultStyle) reads them for
every element and cross-fades per state (:hover { background-image: ... }), so a
4-state bitmap button or a skinned window needs no custom drawer. auto = native size
in logical units (scaled by globalScale, like font-size), centered in the box.
- Buttons (plain and toggle) went back onto the common box path:
filland its hover/pressed/disabled blends are drawn there, the button drawer adds only the bevel, focus ring and caption. A skinned button gets no bevel/focus ring. - All box keys blend for
pressedanddisabledtoo (previously onlyhover). focusedis now a synced state (:focused { ... }works for any element).- Window:
background-imageturns a window into a skinned one — the drawer paints only the image (no fill/header/frame/caption); hit-testing usesdragRegion(nil = whole window is the drag area).TUIWindow.IsSkinnedtells. - Removed:
TUIWindow.background:pointer(the skin is a style value now) andSetupWindow(wnd,TTiledImage)(nothing ever drew it). SetupSkinnedWindow(wnd,img)now setsbackground-image: tex:<img.name>(naming the textureskin:<wnd.name>if it has no name) and the window size; the old implicit$FF808080tint is the default anyway. Add state variants throughwnd.style.Apus.Engine.CustomStyleno longer draws windows (they render through the box path).- New helper in DefaultStyle:
ResolveStyleImage(src)— shared byTUIImage.srcand the box path; textures are memoized per element in the drawer context.
Style attribute names are now canonical (kebab-case, CSS names where CSS has the
concept); the full list lives in the header of Apus.Engine.Style.pas. Drawers read
only canonical keys — the old spellings are silently ignored (no aliases).
coloris the content (text/glyph) color everywhere and inherits down the element tree. It is never a box color.- Box chrome keys (
fill,border-*,radius,inner-*) never inherit. Onlyfont,font-size,color,text-align,text-shadow,text-decorationcascade from parents (IsInheritedStyleKey); previously every key cascaded. - Style keys are string literals, so
engine5.upgradecannot rewrite them — update style text by hand:
| widget | old key | new key |
|---|---|---|
| button | color (box) |
fill |
| button | text-color |
color |
| checkbox/radio | col (box outline) |
border-color |
| checkbox/radio | text-color |
color |
| checkbox/radio | tick-col |
tick-color |
| frame | color, color-dark |
border-color, border-dark |
| image | color (tint) |
tint |
| window | color / col |
fill |
| editbox | col |
color |
| editbox class default | borderWidth, borderColor |
border-width, border-color (the old spelling was never read, so edit boxes now get their default border) |
| scrollbar | col, track-col, active-col |
color, track-color, active-color |
| scrollbar | style: flat |
variant: flat |
| any | inner-border |
inner-border-color |
| any | border (never read) |
border-color |
| listbox (style, not yet read) | sel-bg, sel-text-color |
:selected { fill; color } |
| transitions | hoverTime, hoverTimeUp, hoverTimeDown |
hover-time, hover-time-in, hover-time-out |
| transitions | pressTime, releaseTime, disableTime, enableTime |
press-time, release-time, disable-time, enable-time |
| any | hover: parent |
hover-target: parent |
- Buttons (plain and toggle) temporarily left the common box path (
DrawCommonStyle); they are back on it since 2026-09-05 (see above). - Removed:
TUIElement.SetStyle(edited the style text without reparsing, so it never took effect). Useelement.style.SetAttr(key,value). - Legacy numeric
styleInfostrings ('60404040','FFB0C0C4 80000000') were never parsed by the R-05 parser; writefill:$60404040instead. TUIEditBox.SetDefault('style',...)/SetDefault('styleInfo',...): onlystyleInfois a real class attribute;stylewas never read.
Time.Ticksnow uses the same high-resolution monotonic source asTime.TicksUs(QPC on Windows,clock_gettimeon Unix-like systems).- Milliseconds are calculated directly with a cached
1000/frequencyscale; the runtime path does not callTicksUsor divide its result by 1000. - This removes the 10-16ms Windows granularity of
GetTickCount64while keepingTime.Ticksno slower thanTime.TicksUs. TestCorechecks for an observed timer step of at most 5ms.BenchCorecompares the raw platform counter withTime.TicksandTime.TicksUs, using ABBA ordering for the two scaled APIs to reduce warm-up/frequency bias.
jni.curVMis now mutable. Android initialization stores thePJavaVMsupplied by JNI soApus.Androidcan attach and detach engine worker threads.
Apus.Core.LineBreakprovides the current platform's native text line terminator: CRLF on Windows and LF on Unix-like systems.- Robot API responses and the engine diagnostics used by Robot API handlers now
use
LineBreak; wire protocols that require CRLF remain unchanged. Apus.LogandApus.Loggingwrite native line endings to their log files and caches instead of forcing Windows CRLF on every platform.
Apus.GlyphCachenow rejects items that cannot fit the concrete atlas region before entering the band allocator. Packed glyph metadata supports dimensions up to 255x255; the older 63x63 comment was stale.- Generic
DGC: cache overflowerrors now distinguish atlas-space exhaustion from exhaustion of internal band descriptors and include the requested band, atlas dimensions, and allocator state. Apus.Engine.TextDrawchecks the fixed glyph-update rectangle list before writing a new entry, preventing a range-check exception in the diagnostic path.
Apus.Socket.recvandsendnow use the Delphi/FPC-compatibleNativeUInttype for buffer length instead of the FPC-specificSizeUInt.Apus.Socket.WSAAcceptusesUIntPtrfor its callback data instead of the FPC-specificPtrUIntspelling.
Apus.HttpRequestsnow launches request workers throughApus.Threads.Thread.Startand tracks them asIThread; the legacyClasses.TThreadsubclass andResumelifecycle are gone.- Unhandled exceptions escaping a
Thread.Startcallback are now written withLog.Force, including the thread name, exception class, raw exception address, available FPC stack-frame addresses, and message. The same diagnostic is kept inIThread.StatusTextbefore the thread enters the error state. Apus.Threadsnow notifiesApus.EventManwhen a thread exits. Any queued or mixed handlers still owned by that thread are removed, its pending events are discarded, and a warning lists each dangling event registration together with its handler address.
- R-07 is confirmed working and merged into
engine5. - Base migration track is complete (100%): no active
Apus.Commondependencies remain in liveBase/*.pasmodules. Base/Apus.Common.paswas removed; compatibility unit is kept only inBase/Deprecated/Apus.Common.pas.- No global/blocking migration tasks remain in Base; current focus is support/development:
- Linux fixes and verification;
- benchmark runs and baseline tracking;
- SSE optimization for highest-impact functions;
- ongoing bugfixes with test coverage expansion.
- Renamed
Apus.Engine.Networking2toApus.Engine.UdpTransport. - The module is the symmetric UDP transport layer: packets/sessions/ping/LAN discovery with
ConnectandAccept. - Updated EngineTest project references and signal inventory docs to use the new name.
- 2026-10-07, lifecycle contract (porting old Networking2 code):
NetInit(port)binds the socket synchronously and strictly: a busy port raisesEError(no silent move toport+100); on return the transport is ready. LAN tools that ran two instances on one host must useNetInit(0)for the second one.NetDonejoins the network thread and closes the socket before returning; a call withoutNetInitis ignored; connections are left disconnected (no notifications).TConnection.Create/Connect/Accept/SendDataraiseEErrorbeforeNetInit: create connections after it.- Session IDs no longer use
System.Random, and the unit no longer callsRandomize. - 2026-10-08: the unit's generator is a
TRandomfromApus.Core(InitUnique).
- Added shared zero-based row-pointer helper types in
Apus.Corefor typed raw-buffer indexing:TByteRow/PByteRowTWordRow/PWordRowTIntRow/PIntRowTCardinalRow/PCardinalRowTSingleRow/PSingleRow
- Motivation: make Delphi/FPC-safe row casts explicit and reusable in image/buffer code, for example
PCardinalRow(data)^[i]. Row types use a large upper bound so indexed access also works with range checking enabled. - Added
TTexture.ScanLine(y):pointerandTTexture.PixelPtr(x,y):pointerinApus.Engine.Resourcesso locked texture code can usePCardinalRow(tex.ScanLine(y))or address a concrete pixel without repeating rawdata+pitcharithmetic.
- Fixed
Apus.Engine.APIdisplay-mode exports for API-only users:TDisplayScaleModenow aliasesApus.Engine.Types.TDisplayScaleMode.
- Fixed Linux SDL mouse-button cursor handling to call
systemPlatform.SetCursor. - Updated
tests/PlatformTest.dprandtests/OpenGL.dprto the currentTWindow/IGraphicsSystemlifecycle and modern Base units instead of deprecatedApus.Common. - Fixed case-sensitive include lookup in
tests/TestStyle.dpr(Test.inc). - Linux/FPC compile-only checks now pass for:
tests/PlatformTest.dprtests/OpenGL.dprdemo/SimpleDemo/SimpleDemo.dprdemo/StyleDemo/StyleDemo.dprdemo/MultiWindow/MultiWindow.dprdemo/Draw2D/Draw2D.dprdemo/InputDemo/InputDemo.dprdemo/UI/UI.dprdemo/TextDemo/TextDemo.dprdemo/Tweenings/Tweenings.dpr
- Linux/FPC runtime check now passes for:
tests/TestStyle.dpr
- Added
tests/linux_smoke.shto run these Linux/FPC checks without leaving build artifacts in the repository.
- Extended
Apus.Socketinto the shared socket compatibility layer for the WinSock-style calls still used by legacy Base network modules. Apus.TCPandApus.SCGInow depend onApus.Socketinstead of importing platform socket units directly.- Unix-only resolver bindings are kept behind
{$IFDEF UNIX}insideApus.Socket. - Linux/FPC compile sweep now includes:
Apus.SocketApus.TCPApus.SCGIApus.Compress
Apus.Profilingremains Windows-only because its implementation directly uses the Windows unit.
- Updated
Base/tests/TestConv.dprto the currentConvAPI:- IP formatting uses
Conv.FormatIp(cardinal). Conv.ToStr(boolean)expectations match the documentedshort=falsedefault.
- IP formatting uses
TestConvnow compiles and passes on Linux/FPC (135checks).- Fixed Linux/FPC thread startup: POSIX
BeginThreadreturns apthread_t, which must not be assigned through the narrowerTHandletype before being stored asTThreadID. TestThreadsnow passes on Linux/FPC (65checks);TestEventManconcurrent thread scenarios also run without range-check exceptions.- Fixed
Apus.EventMan.Linkto preserve the linked event name casing passed by the caller. Matching remains case-insensitive, while handlers now receive the same spelling behavior as directSignalcalls. TestEventMannow passes on Linux/FPC (39checks).- Fixed zero-length
TVec2.NormalizeandTVec2d.Normalizeto produce explicit NaN coordinates without executing runtime0/0.IsValidnow usesIsNanfor the first coordinate instead of a trap-prone NaN self-comparison. TestGeom2Dnow passes on Linux/FPC (78checks).- Disabled Win64-ABI-only
TMat4SSE helpers on Linux x64 so System V builds use the existing Pascal fallback for matrix multiplication and Vec4 point/normal transforms. TestGeom3Dnow passes on Linux/FPC (80checks).- Fixed
Base/tests/test.shto return compilation and runtime failures to its caller. AddedBase/tests/test_all.shfor the deterministic Linux-compatible Base regression set. - Implemented SDL shared OpenGL context lifecycle for secondary windows,
including cross-thread handoff, context sharing, reactivation, and
per-context VAO setup for core profiles. Verified
MultiWindowsecondary startup and shutdown on Linux/WSL with a liveTool 1window. - Added
cthreadsas the first unit for FPC/Unix builds of maintained demo entrypoints so engine applications can start thread-backed game loops on Linux. - Implemented SDL scan-code to virtual-key mapping instead of returning an
uninitialized value from
TSDLPlatform.MapScanCodeToVirtualKey. - Fixed SDL controller initialization to skip
InitJoystick(0)when no joysticks are connected. - Fixed SDL screen-coordinate helpers:
ScreenToClientnow subtracts the native window position, andGetMousePosusesSDL_GetGlobalMouseState. - Made SDL window termination state per-window and routed close events by SDL
window ID, so closing a secondary window does not terminate every window.
Verified on Linux/WSL by closing a live
Tool 1window while the main loop continued running.
- Extended
TRect2with point-containment methods:Contains(const p:TPoint):booleanContains(x,y:single):boolean
- Motivation: remove duplicated inline point-in-rect checks in Engine code and standardize containment logic in the geometry type itself.
- Engine usage updated to call type methods directly (
TWindow.MouseInRect/MouseWasInRect), avoiding unit-local helper functions.
- Added explicit high-precision monotonic time API in
Apus.Core.Time:Time.TicksUs:int64— microseconds since program start (QPC/clock_gettime based)Time.TicksSec:double— seconds since program start (QPC/clock_gettime based)
- All three functions use a high-resolution monotonic source. Choose the unit needed by the caller: milliseconds, microseconds, or seconds.
- Added benchmark coverage in
Base/tests/BenchCore.dpr:Time.TicksTime.TicksUsTime.TicksSec
- Added optional monotonic time override hook in
Apus.Corefor deterministic time-based tests. - API is available only under
{$IFDEF TIME_OVERRIDE}:Time.Override(timeUs:int64)wheretimeUsis microseconds since program start.
- Time behavior under
TIME_OVERRIDE:- if override value is non-zero,
Time.TicksUsreturns it directly; Time.Ticksis derived from override astimeUs div 1000;Time.TicksSecis derived from override astimeUs * 1e-6;- if override is zero, normal platform clock path is used.
- if override value is non-zero,
- With
TIME_OVERRIDEundefined, override code is not compiled and has zero runtime overhead. - Test harness integration:
Base/tests/test.batandBase/tests/test.shnow compile tests with-dTIME_OVERRIDEby default.Base/tests/TestCore.dprincludes explicitTime overridecoverage.Base/tests/TestTweenings.dpruses virtual time path underTIME_OVERRIDE(with fallback toSleepwhen define is off).
- Added
Base/tests/BenchAnimation.dpras a unified benchmark comparingTTweeningandTAnimatedValueon matched scenarios:- active-animation read path (linear/easeOut);
- retarget path with realistic overlap cap (<=3).
- Added
Animationto available benchmark list inBase/tests/README.md.
- Clarified and documented rounding helper intent in
Apus.Core:PRoundis symmetric nearest (away from zero on.5);SRoundis rendering-stable nearest defined asfloor(v+0.5).
- Updated non-ASM
SRoundfallback to match SSE behavior exactly (floor(v+0.5)), replacing oldtrunc(v+0.5)fallback. - Motivation:
- keep identical rounding behavior across CPU/compiler paths;
- preserve translation invariance used by rendering math:
SRound(x+1)=SRound(x)+1.
TFileHandleinApus.Filesis now an opaque record with object-style methods:Read(buf,size):integerWrite(buf,size):integerSeek(offset,origin):int64Close
- These methods are thin wrappers over existing
Files.Read/Write/Seek/Closeand keep provider-chain behavior unchanged. - Existing static handle API in
Filesremains available for compatibility. - Added ergonomic untyped-buffer overloads:
Read(var buf; size:integer)Write(const buf; size:integer)inTFileHandle.
- Safety tweak: raw-pointer access in
TFileHandleis explicit (ReadMem/WriteMem) to avoid overload ambiguity with pointer-value serialization (Write(ptr,sizeof(ptr))writes pointer value viaconst bufoverload).
- Fixed
SetEventHandlerdeduplication identity for concurrent registrations:- before: duplicate check used only
(event,handler); - now: duplicate check uses
(event,handler,threadNum,mode).
- before: duplicate check used only
- Effect: the same handler can be registered for the same event in multiple threads when using
emQueued/emMixed, and each thread receives its own queued callback. - Added regression coverage in
Base/tests/TestEventMan.dpr:- concurrent unique-handler registration;
- concurrent duplicate registration (same thread identity semantics);
- queued same-handler registration in many threads (one callback per thread).
- Restored legacy command-line helpers in
Apus.Utils:HasParam(const name:string):booleanGetParam(const name:string):string
- Source parity note: implementation moved from deprecated
Apus.Common(SameTextsearch acrossParamStr). - Engine integration:
Apus.Engine.GameApp.Preparenow usesHasParam/GetParamfor platform selection (-SDL,-WINDOWS,-PLATFORM=...).
- Added new base module:
Base/Apus.Spatial.pas. - Introduced engine5-first geometry names and spatial primitives:
TVec2,TVec3(12 bytes),TVec4TMat2,TMat3,TMat34,TMat4TRay,TSphere,TFrustum
- Added methods-first intersection API:
TRay.IntersectsSphere/IntersectsBox/IntersectsTriangleTSphere.ContainsPoint/IntersectsSphere/IntersectsBoxTFrustum.InitFromMVP/IntersectsSphere/IntersectsBoxTFrustum.InitFromMVP(...;includeNearFar:boolean=true)supports 4-plane mode for parallel/orthographic projection culling.
- Extended existing box type via helper:
TBBox3sHelper.IncludePoint/IncludeBox/Center/Extents/ContainsPoint/IntersectsBox/IntersectsSphere
- Added scoped static helpers in
TSpatial(Dot3,Cross3,Distance2) to avoid global function sprawl. - Compilation check:
Apus.Spatial.pasbuilds with FPC in Delphi mode.
Files.LoadAsString(fname,...)now strips UTF-8 BOM when reading from file start (startFrom=0).Files.Save(fname,data:String8)now writes UTF-8 BOM by default.- Added overload:
Files.Save(fname,data:String8;addBOM:boolean)for explicit control.- Use
addBOM=falsefor raw text/binary-like writes where byte-exact layout matters.
- Use
- Added
GetEnumNameSafe(typeInfo:pointer;value:integer):stringtoApus.Utils. - Behavior matches legacy
Apus.Common:- returns
GetEnumName(PTypeInfo(typeInfo),value)whentypeInfo<>nil - returns fallback string
ENUM_<value>whentypeInfo=nil
- returns
- Migration note: replace legacy implicit access via
Apus.Commonwith explicituses Apus.Utils.
Apus.FreeTypeFontpublic text API migrated from UTF-16 (WideString/WideChar) to UCS-4 (String32/Char32):RenderText(...;st:String32;...)GetTextWidth(st:String32;...)Interval(ch1,ch2:Char32;...)CharPadding(ch:Char32;...)RenderGlyph(ch:Char32;...)
- Internal hash records now store
Char32andintegermetric fields to avoid truncation. - FreeType calls now pass full codepoints (
cardinal(ch)) instead of 16-bit truncation.
- Added
String32Helper.TryAnsiChar(index:integer;out ch:AnsiChar):boolean(0-based). - Added
String32Helper.AnsiChar(index:integer;defaultChar:AnsiChar=#0):AnsiChar(0-based). - Purpose: simplify migration of parser code that operates on ASCII tags while input is
String32.
- Added
String32Helper.Split(const delimiters:String32;quoteChar:UCS4Char=0):Strings32. - Behavior mirrors
String8.Split(const delimiters:String8;quoteChar:AnsiChar=#0):- split by any delimiter char from the set
- ignore delimiters while inside quoted fragments
- keep quote characters in resulting tokens
- Added
IntArrayHelperinApus.Corewith methods:IndexOf(value:integer):integerContains(value:integer):booleanAdd(value:integer)Remove(value:integer):boolean
- Migration note: replace legacy
FindInteger(arr,v)witharr.IndexOf(v).
TLock.Enternow accepts optional parameter:procedure Enter(callerAddr:pointer=nil);- Use this in wrapper helpers to pass caller from higher stack frame, so lock diagnostics store meaningful owner/trying addresses.
IndexOf/LastIndexOf/ContainsforString8andString32now support optionalignoreCase.- Migration note:
PosFrom(..., ignoreCase=true)must be rewritten manually tost.IndexOf(..., true)(tool marks TODO).
Apus.HtmlTreepublic API is nowString8-based (ParseHTML,DecodeHTMLString, node text/tag/attributes helpers).- Local
PosFromhelper removed; module usesString8.IndexOf(...,ignoreCase)fromApus.Strings.
Fixed incorrect return types that could truncate values:
| Old signature | New signature |
|---|---|
Min(a,b:cardinal):integer |
Min(a,b:cardinal):cardinal |
Min(a,b:int64):integer |
Min(a,b:int64):int64 |
Min(a,b:uint64):integer |
Min(a,b:uint64):uint64 |
Max(a,b:cardinal):integer |
Max(a,b:cardinal):cardinal |
Max(a,b:int64):integer |
Max(a,b:int64):int64 |
Max(a,b:uint64):integer |
Max(a,b:uint64):uint64 |
Min(a,b,c:single):integer |
Min(a,b,c:single):single |
Max(a,b,c:single):integer |
Max(a,b,c:single):single |
Implementation of Min/Max(a,b,c:single) was also fixed to compare single values directly (no trunc()).
Global free functions replaced with static methods on type Color = record.
BilinearMixF (scalar) and BilinearMix(values:PCardinal,u,v) (SSE pointer overload) remain free functions.
Color, TARGBColor, InvalidColor are re-exported from Apus.Lib.
Gotcha: Pascal is case-insensitive — a local var named color:cardinal shadows the Color type.
Rename any such local to col or a more specific name before calling Color.XXX().
| Old name | New name (Color record) | Notes |
|---|---|---|
MyColor(r,g,b) |
Color.RGB(r,g,b) |
|
MyColor(a,r,g,b) |
Color.ARGB(a,r,g,b) |
|
MyColorF(a,r,g,b) |
Color.ARGBf(a,r,g,b) |
|
GrayColor(gray) |
Color.Gray(gray) |
|
GrayAlpha(alpha) |
Color.GrayAlpha(alpha) |
|
GetAlpha(color) |
Color.Alpha(color) |
|
IsSemiTransparent(color) |
Color.HasAlpha(color) |
|
SwapColor(color) |
Color.Swap(color) |
|
ColorAdd(c1,c2) |
Color.Add(c1,c2) |
|
ColorSub(c1,c2) |
Color.Sub(c1,c2) |
|
ColorMult2(c1,c2) |
Color.Mult(c1,c2) |
|
ColorAlpha(color,alpha) |
Color.Scale(color,alpha) |
multiplies alpha channel |
ReplaceAlpha(color,alpha) |
Color.SetAlpha(color,alpha) |
replaces alpha channel |
ColorMix(c1,c2,value:integer) |
Color.Mix(c1,c2,value:integer) |
|
ColorMixF(c1,c2,t:single) |
Color.Mix(c1,c2,t:single) |
|
ColorBlend(c1,c2,value:integer) |
Color.Blend(c1,c2,value:integer) |
|
ColorBlendF(c1,c2,value:single) |
Color.Blend(c1,c2,value:single) |
|
Blend(background,foreground) |
Color.Blend(background,foreground) |
alpha-blend overload |
BilinearMix(c0,c1,c2,c3,u,v) |
Color.BilinearMix(c0,c1,c2,c3,u,v) |
4-arg cardinal overload |
BilinearBlend(c0,c1,c2,c3,v1,v2) |
Color.BilinearBlend(c0,c1,c2,c3,v1,v2) |
|
Lightness(color) |
Color.Lightness(color) |
|
ColorDiff(c1,c2) |
Color.Diff(c1,c2) |
|
SimpleColorDiff(c1,c2) |
Color.SimpleDiff(c1,c2) |
|
Brightness(c,value) |
Color.Brighten(c,value) |
renamed |
Contrast(c,value) |
Color.Contrast(c,value) |
same name |
BilinearMixF(v0,v1,v2,v3,u,v) |
BilinearMixF(...) |
stays free function |
BilinearMix(values:PCardinal,u,v) |
BilinearMix(values:PCardinal,u,v) |
stays free function (SSE) |
| Old name (Common) | New name (Core) | Notes |
|---|---|---|
min2(a,b) |
Min(a,b) |
overloads for integer/single/double |
max2(a,b) |
Max(a,b) |
overloads for integer/single/double |
min2d(a,b) |
Min(a,b:double) |
merged into Min overloads |
max2d(a,b) |
Max(a,b:double) |
merged into Max overloads |
min2s(a,b) |
Min(a,b:single) |
merged into Min overloads |
max2s(a,b) |
Max(a,b:single) |
merged into Max overloads |
Clamp(v,min,max) |
Clamp(v,min,max) |
same name, added single/double overloads |
Sat(v,0,255) |
Sat(v) |
now clamps to 0..1 (single/double) |
Swap(a,b) |
Swap(a,b) |
same name |
GetPow2(v) |
NextPow2(v) |
renamed for clarity (next power of two) |
Pow2(e) |
Pow2(e) |
same name |
Log2i(v) |
Log2i(v) |
same name |
HasFlag(v,flag) |
Bits.HasAll(v,flag) |
|
SetFlag(v,flag) |
Bits.SetFlag(v,flag) |
|
GetBit(data,idx) |
Bits.Get(data,idx) |
|
SetBit(data,idx,val) |
Bits.SetBit(data,idx,val) |
|
GetBits(data,idx,size) |
Bits.GetBits(data,idx,size) |
overload for cardinal |
SetBits(data,idx,size,val) |
Bits.SetBits(data,idx,size,val) |
overloads for byte/word/cardinal/uint64 |
ZeroMem(data,size) |
Mem.Clear(data,size) |
|
Toggle(b) |
Toggle(b) |
same name |
Wrap(v,max:single) |
Wrap(v,max:single) |
same name; with fast path for in-range values |
Wrap(v,max:double) |
Wrap(v,max:double) |
same name |
FRound(v:double) |
FRound(v:double) |
same name; fast round (biased +epsilon) |
PRound(v:double) |
PRound(v:double) |
same name; precise round |
SRound(v:single) |
SRound(v:single) |
same name; SSE-accelerated, Pascal fallback for ARM |
HasValue(v) |
HasValue(v) |
moved from Apus.Conv to Apus.Core; checks if variant is not unassigned |
Namespace-style API for stack inspection:
| Method | Signature | Notes |
|---|---|---|
Stack.Caller |
class function Caller: pointer |
Get immediate caller address using system API. For fast caller use intrinsics: {$IFDEF FPC}get_caller_addr(get_frame){$ELSE}System.ReturnAddress{$ENDIF} |
Stack.Trace |
class function Trace(var frames: TCallStack; skip: integer = 0): integer |
Capture up to 4 call stack frames. Returns actual count. Used in exceptions/logging. |
Types:
TCallStack = array[0..3] of pointer— fixed array for 4 stack frames
Performance: Stack.Caller is ~50-200 cycles (system call). Intrinsic version is ~3-7 cycles but requires inline usage.
Exception classes moved from Apus.Classes to Apus.Core:
| Old location (Classes) | New location (Core) | Notes |
|---|---|---|
TBaseException |
TBaseException |
same name, base class with stack trace using Stack.Trace |
EWarning |
EWarning |
same name |
EError |
EError |
same name |
EFatalError |
EFatalError |
same name |
Note: In engine5, EBaseException uses Stack.Trace to capture call stack on all platforms (previously only worked on x86 32-bit). Exception message format: [addr1->addr2->addr3] Error message
Unified time API replacing fragmented functions:
| Old name | New name (Time) | Notes |
|---|---|---|
GetTickCount (32-bit) |
Time.Ticks |
REMOVED — old 32-bit version (overflow every 49 days) |
GetTickCount64 |
Time.Ticks |
64-bit monotonic time in ms, cross-platform |
MyTickCount |
Time.Ticks |
better replacement with no overflow |
Sleep(ms) |
Time.Sleep(ms) |
moved to Time scope |
Time.Now |
Time.Now |
same — high-precision local datetime |
Time.UTC |
Time.UTC |
same — high-precision UTC datetime |
Time.Stamp |
Time.Stamp |
same — HH:MM:SS.mmm for logs |
High-resolution timer (Timer scope in Apus.Core):
Timer.Start(out t)/Timer.Get(t):double— explicit timer, returns secondsTimer.Start/Timer.Get— implicit internal timer (parameterless)
Exception helper functions moved from Apus.Common to Apus.Core:
| Old location (Common) | New location (Core) | Notes |
|---|---|---|
ExceptionMsg(e) |
ExceptionMsg(e) |
Returns the message with a raw address chain safe to capture inside any worker thread. For EBaseException uses its already captured stack; for regular FPC exceptions includes ExceptAddr and available ExceptFrames. |
NotImplemented(msg) |
NotImplemented(msg) |
Raises EError with "Not implemented: msg". Inline. |
NotSupported(msg) |
NotSupported(msg) |
Raises EError with "Not supported: msg". Inline. |
High-precision time functions:
| Old name (Common) | New name (Core) | Notes |
|---|---|---|
NowGMT |
Time.UTC |
UTC time in TDateTime format (high-precision on Windows 8+) |
| — | Time.Now |
Local time in TDateTime format (high-precision on Windows 8+) |
GetUTCTime + formatting |
Time.Stamp |
Returns HH:MM:SS.mmm string for logs |
MyTickCount |
Time.Ticks |
high-resolution monotonic milliseconds, cross-platform |
Usage:
dt := Time.UTC; // high-precision UTC
dt := Time.Now; // high-precision local time
Log.Msg(Time.Stamp + ' Started');Use CoreTime.Ticks when SysUtils.Time creates a name conflict in a unit.
| Old name (Common) | New name (Conv) | Notes |
|---|---|---|
ParseInt(st) |
Conv.ToInt(st) |
|
ParseFloat(st) |
Conv.ToFloat(st) |
|
ParseBool(st) |
Conv.ToBool(st) |
|
HexToInt(st) |
Conv.HexToInt(st) |
same name |
StrToIp(st) |
Conv.ToIp(st) |
|
IpToStr(ip) |
Conv.FormatIp(ip) |
renamed from Conv.ToIp(cardinal) to Conv.FormatIp |
FormatHex(v,digits) |
Conv.ToHex(v,digits) |
|
PtrToStr(p) |
Conv.ToStr(p) |
|
BoolToAStr(b) |
Conv.ToStr(b) |
|
FormatInt(v) |
Conv.FormatInt(v) |
same name |
FormatMoney(v,digits) |
Conv.FormatMoney(v,digits) |
same name |
SizeToStr(size) |
Conv.FormatSize(size) |
|
FormatTime(timeMs) |
Conv.TimeToStr(timeMs) |
|
EncodeHex(data,size) |
Conv.EncodeHex(data,size) |
same name |
DecodeHex(hexStr) |
Conv.DecodeHex(hexStr) |
same name |
HexDump(buf,size) |
Conv.HexDump(buf,size) |
same name |
DecDump(buf,size) |
Conv.DecDump(buf,size) |
same name |
EncodeB64(data,size) |
Conv.ToBase64(data,size) |
|
DecodeB64(st,buf,size) |
Conv.FromBase64(st,buf,size) |
String functions moved from free-standing functions to String8Helper record helper.
Call style changes from Func(st, args) to st.Method(args).
| Old name (Common) | New name (Strings) | Notes |
|---|---|---|
PosFrom(substr,st) |
st.IndexOf(substr) |
For ignoreCase=true, use manual rewrite to st.IndexOf(substr,1,true) |
PosFrom(substr,st,minIdx) |
st.IndexOf(substr,minIdx) |
For ignoreCase=true, use manual rewrite to st.IndexOf(substr,minIdx,true) |
LastPos(substr,st) |
st.LastIndexOf(substr) |
|
Chop(st) |
st.Trim |
|
SameText8(a,b) |
a.Same(b) |
case-insensitive |
UpperCase(st) |
st.ToUpper |
|
LowerCase(st) |
st.ToLower |
|
SplitA(delim,st) |
st.Split(delim) |
CAUTION: Split treats each char as delimiter; SplitA uses whole string |
Join(arr,delim) |
String8.Join(arr,delim) |
class function |
QuoteStr(st) |
st.Quote |
|
UnQuoteStr(st) |
st.Unquote |
|
URLEncodeUTF8(st) |
st.UrlEncode |
|
ExtractStr(st,pre,suf) |
st.Extract(pre,suf) |
|
ParseInt(st) |
st.ToInt64 |
also available via Conv.ToInt |
ParseFloat(st) |
st.ToDouble |
also available via Conv.ToFloat |
ParseBool(st) |
st.ToBoolean |
also available via Conv.ToBool |
FastHash(st) |
FastHash(st) |
same name, simple fast hash (case-insensitive) |
StrHash(st) |
StrHash(st) |
same name, string hash (case-sensitive) |
SameText8(a,b) |
a.Same(b) |
case-insensitive comparison |
Format(fmt,args) |
UTF8.Format(fmt,args) |
native String8 format, no Unicode roundtrip. Specs: %d %u %x %X %f %g %s %p %%, flags: - 0 +, width, .precision |
| Old name (Common) | New name (Strings) | Notes |
|---|---|---|
Str8(st) |
Str8(st) |
same name; overloads for UnicodeString, WideString, UTF8String (+AnsiString in UNICODE mode) |
Str16(st) |
Str16(st) |
same name; overloads for UnicodeString, WideString, UTF8String (+AnsiString in UNICODE mode) |
| — | Str32(st) |
NEW: convert String8/WideString/UnicodeString to String32 (UCS-4) |
| Old name (Common/Conv) | New name (Strings) | Notes |
|---|---|---|
EncodeUTF8(ws) |
UTF8.Encode(ws) |
WideString → String8; also accepts addBOM parameter |
DecodeUTF8(s) |
UTF8.ToWide(s) |
String8 → WideString; strips BOM if present |
IsUTF8(s) |
UTF8.HasBOM(s) |
check for UTF-8 BOM |
UTF8.FromWide(ws) |
UTF8.FromWide(ws) |
alias for UTF8.Encode (inline) |
| Old name (Common/Conv) | New name (Utils) | Notes |
|---|---|---|
UnicodeTo(ws,enc) |
UnicodeTo(ws,enc) |
WideString → String8 with arbitrary encoding (UTF-8/Win1251/ANSI) |
UnicodeFrom(s,enc) |
UnicodeFrom(s,enc) |
String8 → WideString with arbitrary encoding |
File functions moved from free-standing functions to Files record with static class methods.
Call style changes from Func(args) to Files.Method(args).
| Old name (Common) | New name (Files) | Notes |
|---|---|---|
MyFileExists(fname) |
Files.Exists(fname) |
|
GetFileSize(fname) |
Files.GetFileInfo(fname,info) |
size via info.size field |
LoadFileAsBytes(fname) |
Files.LoadAsBytes(fname) |
added numBytes, startFrom params |
LoadFileAsString(fname) |
Files.LoadAsString(fname) |
added numBytes, startFrom params |
SaveFile(fname,buf,size) |
Files.Save(fname,buf,size) |
also overloads for ByteArray, String8, TBuffer |
ReadFile(fname,buf,posit,size) |
Files.ReadBlock(fname,buf,offset,size) |
|
WriteFile(fname,buf,posit,size) |
Files.WriteBlock(fname,buf,offset,size) |
| Old name (Common) | New name (Files) | Notes |
|---|---|---|
CopyFile(sour,dest) |
Files.CopyFile(sour,dest) |
same name |
MakeBakFile(fname) |
Files.MakeBakFile(fname) |
same name |
| Old name (Common) | New name (Files) | Notes |
|---|---|---|
DirectoryExists(path) |
Folder.Exists(path) |
new wrapper |
ForceDirectories(path) |
Folder.Create(path) |
creates with parents |
ListFiles(path,mask,recursive) |
Folder.ListFiles(path,mask,recursive) |
mask supports ;-separated patterns |
FindFile(name,path) |
Folder.Find(name,path) |
findDir=false by default |
FindDir(name,path) |
Folder.Find(name,path,true) |
findDir=true |
CopyDir(sour,dest) |
Folder.Copy(sour,dest) |
|
MoveDir(sour,dest) |
Folder.Move(sour,dest) |
|
DeleteDir(path) |
Folder.Delete(path) |
| Old name (Common) | New name (Files) | Notes |
|---|---|---|
SafeFileName(fname) |
Files.SafeFileName(fname) |
same name |
FileName(fname) |
Files.FileName(fname) |
same name |
AddFileNameRule(rule) |
Files.AddFileNameRule(rule) |
same name |
IsPathRelative(fname) |
Files.IsPathRelative(fname) |
same name |
WaitForFile(fname,delay,exists) |
Files.WaitForFile(fname,delay,exists) |
same name |
| Old name (Common) | Notes |
|---|---|
DumpDir(path) |
depends on ForceLogMessage (logging) |
Logging functions extracted from Apus.Common into new unified API.
| Old name (Common) | New name (Log) | Notes |
|---|---|---|
LogMessage(text) |
Log.Msg(text) or Log.Info(text) |
default severity is Normal (was Normal in Common) |
LogMessage(text,group) |
Log.Msg(text,group) |
category parameter |
LogMessage(text,params) |
Log.Msg(text,params) |
Format overload |
ForceLogMessage(text) |
Log.Force(text) |
forced (bypass cache, flush immediately) |
LogError(text) |
Log.Error(text) |
error level + counter increment |
DebugMessage(text) |
Log.Debug(text) |
debug level (lowest) |
| — | Log.Warn(text) |
new: warning level |
| — | Log.Fatal(text) |
new: fatal error level |
| Old name (Common) | New name (Log) | Notes |
|---|---|---|
UseLogFile(name) |
Logger.UseLogFile(name) |
same name, added useThread and keepOpened params |
SetLogMode(mode,groups) |
Logger.SetVerbosity(minSeverity) |
simplified: only min severity filter |
FlushLog |
Logger.Flush |
same functionality |
LogCacheMode(enable,enforce,runThread) |
Logger.LogCacheMode(enable,bypassSeverity) |
redesigned params |
StopLogThread |
Logger.StopLogThread |
same name |
| — | Logger.SetCustomHandler(handler,disable) |
NEW: interceptor support for Apus.Logging refactor |
| — | Logger.GetErrorCount |
NEW: returns error/fatal counter |
Replaces old TLogModes (lmSilent/lmForced/lmNormal/lmVerbose):
| Old (Common) | New (Log) | Value | Notes |
|---|---|---|---|
| — | TSeverity.Debug |
0 | auxiliary debug info |
| — | TSeverity.Info |
1 | minor event |
| LogMessage default | TSeverity.Normal |
2 | regular event |
| ForceLogMessage | TSeverity.Forced |
3 | important, flush buffer |
| — | TSeverity.Warn |
4 | warning |
| LogError | TSeverity.Error |
5 | error |
| — | TSeverity.Fatal |
6 | fatal error |
Threading primitives and thread management utilities extracted from Apus.Common.
Major changes in engine5:
- Type renamed: TMyCriticalSection → TLock (modern, cross-platform)
- Fields encapsulated: TLock fields are now private
- New scope:
Threadrecord for thread operations - New methods: TLock.Init/Cleanup, IsLocked, GetOwner
- New type: TLightweightEvent (WaitOnAddress/futex-based)
| Old name (Common) | New name (Threading) | Notes |
|---|---|---|
InitCritSect(cr,name,level) |
InitCritSect(cr,name,level) |
same name |
DeleteCritSect(cr) |
DeleteCritSect(cr) |
same name |
EnterCriticalSection(cr,caller) |
EnterCriticalSection(cr,caller) |
same name |
LeaveCriticalSection(cr) |
LeaveCriticalSection(cr) |
same name |
DumpCritSects |
DumpCritSects |
same name |
CheckCritSections |
CheckCritSections |
same name |
TMyCriticalSection.Enter |
lock.Enter |
method restored, same syntax |
TMyCriticalSection.Leave |
lock.Leave |
method restored, same syntax |
| Old function (Common) | New method (Threads) | Notes |
|---|---|---|
RegisterThread(name) |
Thread.Register(name) |
static method, cleaner |
UnregisterThread |
Thread.Unregister |
static method |
PingThread |
Thread.Ping |
static method |
GetThreadName(threadID) |
Thread.GetName(threadID) |
static method, 0=current |
Note: Old functions still available but deprecated for compatibility.
| Method | Notes |
|---|---|
lock.Init(name, level) |
Initialize lock (replaces InitCritSect) |
lock.Cleanup |
Cleanup lock (replaces DeleteCritSect) |
lock.Enter |
Acquire lock |
lock.Leave |
Release lock |
lock.IsLocked |
Check if locked |
lock.GetOwner |
Get owning thread ID |
| Old name (Common) | New name (Threading) | Notes |
|---|---|---|
WaitFor(var p,maxTime) |
WaitFor(var p,maxTime) |
same name |
debugCriticalSections |
debugCriticalSections |
global var, same name |
| Old type (Types/Common) | New type (Threads) | Notes |
|---|---|---|
TMyCriticalSection |
TLock |
RENAMED and moved from Apus.Types to Apus.Threads |
PCriticalSection |
PLock |
RENAMED pointer type |
TSRWLock |
TSRWLock |
MOVED from Apus.Types to Apus.Threads (Windows Vista+) |
| Type | Description |
|---|---|
TLightweightEvent |
Lightweight event using WaitOnAddress (Win8+) or futex (Linux) |
TScopedLock |
Delphi-only RAII lock wrapper with automatic cleanup ({$IFDEF DELPHI}) |
Thread |
Static record for thread operations |
| Old name | New name | Notes |
|---|---|---|
TMyCriticalSection |
TLock |
type alias |
PCriticalSection |
PLock |
type alias |
TCriticalSection |
TLock |
type alias |
PCS |
PLock |
type alias |
RegisterThread |
Thread.Register |
old function deprecated |
UnregisterThread |
Thread.Unregister |
old function deprecated |
PingThread |
Thread.Ping |
old function deprecated |
GetThreadName |
Thread.GetName |
old function deprecated |
Functions that don't fit scope of core/conv/strings/files modules.
| Old name (Common) | New name (Utils) | Notes |
|---|---|---|
ParseDate(st,default) |
ParseDate(st,default) |
same name - parses DD.MM.YYYY HH:MM:SS and variants |
ParseTime(st,default) |
ParseTime(st,default) |
same name - parses HH:MM:SS |
GetDateFromStr(st) |
GetDateFromStr(st) |
alias for ParseDate |
SplitA(divider,st) |
SplitA(divider,st) |
same name - splits by string divider (whole string, not charset) |
SplitA(divider,st,quotes) |
SplitA(divider,st,quotes) |
same name - with quote handling |
Chop(st) |
Chop(st) or st.Trim |
both available - trim whitespace |
Note: Utils is the default place for functions that don't fit other modules. More functions will be added here (EncodeUTF8/DecodeUTF8, AddString/RemoveString, HasParam/GetParam, etc).
Compression/decompression and binary patching utilities extracted from Apus.Common.
| Old name (Common) | New name (Compress) | Notes |
|---|---|---|
PackRLE(buf,size,addHeader) |
RLE.Pack(buf,size,addHeader) |
|
UnpackRLE(buf,size) |
RLE.Unpack(buf,size) |
|
CheckRLEHeader(buf,size) |
RLE.CheckHeader(buf,size) |
returns unpacked size or -1 |
| Old name (Common) | New name (Compress) | Notes |
|---|---|---|
SimpleCompress(data) |
LZ.Compress(data) |
|
SimpleDecompress(data) |
LZ.Decompress(data) |
| Old name (Common) | New name (Compress) | Notes |
|---|---|---|
CreateBackupPatch(orig,mod,size) |
Patch.Create(orig,mod,size) |
|
ApplyBackupPatch(data,size,patch,patchSize) |
Patch.Apply(data,size,patchBuf,patchSize) |
This section tracks old Apus.Common helpers that still matter for migration.
Do not treat it as a list of functions to reintroduce under the old names.
| Old name | Replacement | Notes |
|---|---|---|
EncodeUTF8(st) / DecodeUTF8(st) |
UTF8.Encode, UTF8.ToWide, UTF8.FromWide, Str8, Str16 in Apus.Strings |
Prefer explicit UTF-8 helpers and string conversion helpers, not standalone Common names. |
UnicodeFrom(st,enc) / UnicodeTo(st,enc) |
Apus.Utils | TTextEncoding and the encoding conversion helpers live together in Apus.Utils. |
Str8(s) / Str16(s) |
Apus.Strings | Cast/convert to String8 / String16. |
SortRecordsByInt/Double/Float |
Apus.Core.Sort | Implemented as Sort.ByInt/ByFloat/ByDouble(var items; itemSize,itemCount,offset:integer; asc:boolean). |
HasParam/GetParam |
Apus.Utils | Command-line argument access. |
TTextEncoding/teUnknown |
Apus.Utils | Defined as TTextEncoding=(teUnknown,teANSI,teWin1251,teUTF8). |
ErrorMessage(msg) |
Apus.Core SystemMessage(msg) |
Configurable critical message output (log/stderr/msgbox/raise). |
LastChar(st) |
Apus.Strings | Available as string helper methods. |
Unescape(st) |
Apus.Strings | Available as String8Helper.Unescape. |
ExtractFilePath/FileName/ExpandFileName |
SysUtils | RTL functions; add SysUtils to uses. |
TrimLeft/TrimRight standalone |
SysUtils or st.TrimLeft/TrimRight |
Also available as String8 helper methods. |
TRandom |
Apus.Core TRandom |
Same idea, finished as a real PCG32: Init(seed[,stream]), new InitUnique (clock+PID+thread ID+stack address+counter; old Init(0) seeded from Now and gave equal sequences to processes started together), Next is the PCG32 output, Float never returns 1.0, Normal keeps the second Box-Muller value. Added Range, Chance, Round. |
RandomInt(v) |
Apus.Core TRandom.Round(v) |
Needs a generator instance; now a proper floor for negative values. |
| Old name | Current status | Notes |
|---|---|---|
IsUTF8(st) |
Not reintroduced as a Common-compatible helper | Old behavior checked for UTF-8 BOM. Prefer explicit BOM handling or UTF8.IsValid, depending on intent. |
SafeStrItem(arr,i) |
Still only in deprecated Common | Add a small helper only when a live migration needs this exact behavior. |
PackBytes(b1..b4) / PackWords(w1,w2) |
Still only in deprecated Common | Candidate for Apus.Core if live users need it. |
PointerInRange(p,base,size) |
Still only in deprecated Common | Candidate for Apus.Core if live pointer-range checks need it. |
AddString/RemoveString/FindString |
Still only in deprecated Common under the old names | Prefer typed dynamic-array helpers or capacity-aware builders for new code. Base/Apus.Android.pas still has legacy-name users behind Android-specific code paths. |
RandomStr(l) |
Not reintroduced | Add to TRandom when a live migration needs it. |
PseudoRand / FillRandom |
Not reintroduced | Deterministic hash-like fillers, not random generators. |
THashItem,TCellTStrHashTHashTSimpleHash,TSimpleHashS,TSimpleHashAS,TSimpleHash8TObjectHash,PObjectHashTVarHash,PVarHashTErrorState
Apus.Structsis removed from live Base.- No compatibility aliases are kept in
Apus.Structs; update old units to useApus.HashMapsorApus.Containersexplicitly. - Hash implementations live in
Apus.HashMaps. - Non-hash algorithmic containers live in
Apus.Containers.
- Preferred generic API for new code:
THashMap<T>. - Supported specialized hashes:
THash(DB-oriented multi-value mode:String8 -> variant(s))TSimpleHash(int64 -> int64)TObjectHash(String8 -> TNamedObject)
- Legacy/compat hashes marked
deprecated:TStrHashTSimpleHashSTSimpleHashASTSimpleHash8TVarHash
- Added new module
Apus.Containersfor non-hash algorithmic containers. - Removed
Apus.Structsunit and moved container API surface toApus.Containers. - Removed legacy
TestStructs; addedTestContainersin modern test format. TBitStreamis now inApus.Types.
- Added
Sortscope inApus.Core:Sort.ByIntSort.ByFloatSort.ByDoubleSort.ByStr(String8field)
- Removed legacy
SortRecordsByInt/Float/DoubleAPI fromApus.Types(no wrappers left). - Moved sorting coverage from
TestTypestoTestCore.
- Added new test suite:
Base/tests/TestTypes.dpr(modern console test template). - Expanded edge-case coverage for:
TIntRange/TFloatRangeTArray<T>TNameValue/TNameValueListTBuffer/TWriteBufferTBitStream
- Fixed defects found by tests:
TBitStream: proper buffer zeroing size, safe bit mask for bit 31 under range/overflow checks, correct resize rounding, correct bit-write path inPut(var buf;...).TNameValueList.Init(st,...): constructor now assigns delegated constructor result toself.
IRenderTarget.UseDepthBuffer(test;writeEnable:boolean)→SetDepthMode(test:TDepthTest=Keep; write:TDepthWrite=Keep). The misleading "buffer" name dropped; test and write mask are now independent axes, each with a keep-current sentinel.TDepthBufferTest→TDepthTest(scoped enum):dbDisabled/dbPass/dbPassLess/dbPassLessEqual/dbPassGreater/dbNever→TDepthTest.Disabled/Pass/Less/LessEqual/Greater/Never+ newTDepthTest.Keep.- New
TDepthWritescoped enum (Keep/Off/On) for the write mask (was a bareboolean).,false→,TDepthWrite.Off,,true→,TDepthWrite.On. - New
IRenderTarget.DepthMode:TDepthModegetter (record{test;write}) + trackedcurDepththreadvar state — enables save/restore and keep-current. No state stack (deliberate). - Fixed latent bug: write mask was skipped when test=
dbDisabled; nowglDepthMaskalways reflects tracked state. - No compatibility alias. Upgrader rules added to
tools/engine5.upgrade.
- Removed
Apus.Engine.PainterGL.pasandApus.Engine.PainterGL2.pas. - Current desktop rendering uses
Apus.Engine.OpenGLwithIGraphicsSystem,IRenderTarget,IShader,IDrawer, andITextDrawer. - The old Android backend still needs migration to the current graphics-system architecture before Android builds can be re-enabled.
Apus.Lognow mirrors important Android messages (Forcedand above) to logcat under theApusEnginetag.- Severity maps to Android's Info/Warn/Error/Fatal priorities. Lower-volume
Debug,Info, andNormalmessages remain in the regular Engine5 log. - No public logging API changed.
Window geometry is now a resolved snapshot instead of a set of loose fields.
Design: Work/R-31_api_design.md.
TWindowgainedconfig:TSurfaceConfig(the axes requested for this window) andsurface:TSurfaceState(the resolved snapshot: client/display/render/canvas sizes, safe area, DPI, mechanism). The snapshot is written only by the window's own thread, between frames.- Renamed:
TWindow.renderWidth/renderHeight→canvasWidth/canvasHeight,windowWidth/windowHeight→clientWidth/clientHeight,screenDPI→surface.dpi.displayRectis now a read-only property. ClientToGame/GameToClient(procedures with avarargument) →ClientToCanvas/CanvasToClientfunctions, plusTryClientToCanvas(false outside the display rect) andMapPointerToCanvas(pointer contract: outside the picture the position is dropped unless a button is held).TGameBase.RenderSize→CanvasSize.- Resize/DPI flow inverted: the platform layer only posts a request
(
RequestResize,RequestDPI,SetNativeSafeArea), and the window's own thread applies it at frame start viaApplyPendingSurface. A tool window resize no longer rebuilds the main window.TGame.SetupRenderArea,TGame.SizeChangedand theENGINE\RESIZEsignal are gone. - Events
ENGINE\BEFORERESIZE,ENGINE\RESIZED,ENGINE\DPICHANGEDandENGINE\DPICHANGED\DONEare replaced by a singleENGINE\SURFACECHANGED(tag=UIntPtr(wnd)), sent after the rebuild; what changed is inwnd.surface.changes. TGameSettings:mode,altModeare plainTDisplayModenow; the axes come assurface:TSurfaceConfig. Removed:TDisplaySettings,TDisplayFitMode(dfm*),TDisplayScaleMode(dsm*).TGameApplication: removedTGameAppMode/gameMode,directRenderOnlyanduseRealDPI(the process is DPI-aware unconditionally). Addedorientation,surfaceConfig, presetsSetupFullWindow/SetupFixedCanvas/SetupMobilePortrait/SetupMobileLandscape/SetupPixelArt, and theConfigureSurfacehook called before every resolve.- The default render target is no longer a global switch: it is created only when
the resolved surface needs one (
surface.needRT), so the full-window default still renders straight to the backbuffer. - Migration:
gamUseFullWindow→ the default,gamKeepAspectRatio→SetupFixedCanvas(w,h). Upgrader rules added totools/engine5.upgrade.
defines.incno longer enables any audio backend on its own. Nothing is linked unless the project builds with-dSDLMIX(SDL2_mixer) or-dIMX(legacy Win32 IMixerPro). TheNOAUDIOopt-out is gone with it.- Rationale: a backend binding is a load-time import, so linking one makes the
executable depend on
SDL2_mixer.dll/libSDL2_mixereven when the project never plays a sound. - With no backend compiled in,
InitSoundSystem(slDefault,...)logs one line and returns instead of raising. An explicit backend request (slSDL,slIMixer) still raises, as before. - Builds that need sound: put
-dSDLMIXin the project'sbuild.cfg(asdemo/SoundDemo/build.cfgdoes) or pass it tobuild.cmd/build.sh.
Files.Open(fname,readOnly)is replaced by four functions, inIFileProvideras well:OpenRead(read-only, must exist),Open(read+write from position 0, created if missing),OpenNew(previous content discarded),OpenAppend(read+write at the end, created if missing). Migration:Open(f,true)→OpenRead(f);Open(f,false)→Open(f)to keep the content, orOpenNew(f)to replace it. A read-only provider only has to serveOpenRead.- Rationale: the boolean carried two different intentions for writers — "rewrite
this file" and "write into the existing one" — and the rewriting callers had
to remember to truncate afterwards.
Files.Save(String8)with the default BOM andFiles.CopyFiledidn't, so writing shorter content left a tail of the previous one in the file. Both are fixed by opening withOpenNew, and the compensatingSetSizeis gone fromFiles.Save. Files.WriteBlockkeeps its "write at an offset" meaning and no longer doubles as a whole-file save: the three engine callers (robotscreenshot,TWindow.CaptureFrame,ImageTools.SaveImage) now useFiles.Save, which truncates. Saving a PNG over a longer one used to leave bytes pastIEND.
The startup globals of Apus.Engine.GameApp became protected fields of
TGameApplication, grouped by meaning, and the project sets them in one place:
the new SetupApplication hook. Design: Work/gameapp_settings_namespaces.md.
- New protected virtual
SetupApplication.Preparecalls it once, before the log, config and platform are set up. Override it, callinheritedfirst, then assign the fields and the surface preset (SetupFixedCanvasetc).Createonly sets the defaults, so the order ofinheritedand the assignments no longer matters for them. A secondPreparecall raisesEError. - Field groups (records with
Init):appSetup:gameTitle→title,appName→storageName,configFileName→configFile,logFileName→logFile;requestBackend:usedPlatform→platform,usedAPI→graphicsAPI. The request is no longer overwritten by the resolved backend;windowSetup:windowWidth/windowHeight→size:TSize,windowedMode→fullscreen(inverted, default false),windowBorderless→borderless,windowSizeable→resizable,scaleWindowSize→scaleForDPI,useSystemCursor→systemCursor;requestGL:glCoreContext→coreContext,glDebugContext→debugContext,glForwardCompatible→forwardCompatible,glPreferHighest→preferHighest,glMinVersionMajor/Minor→minMajor/minMinor;renderSetup:noVSync→vSync(inverted, default true),useDepthTexture→depthTexture;startupScenes:useConsoleScene→console,useTweakerScene→tweaker,useDefaultLoaderScene→loader.
debugModeandcheckForSteamare plain protected fields now.- Removed:
scaleScenes,scaleFontsandconfigDir(never read),useCustomStyle(led toNotImplemented). - Still global:
deviceDPI,deviceScale,instanceID,gameLangCode. appSetup.configFilekeeps the oldconfigFileNamebehaviour: afterPrepareit holds the bare file name used as the control file key. (Changed on 2026-10-23: it holds the path of the file used, see "Configs: Apus.Config".)- Config keys (
Options\FullScreen,Options\scaleWindowSize, ...) and command line options are unchanged. - Migration: move the assignments from the constructor (or from the program,
before
Create) intoSetupApplication. A program that runs a bareTGameApplicationneeds a small descendant (seedemo/01-Scenes). Example:windowWidth:=1280; windowHeight:=720;→windowSetup.size:=MakeSize(1280,720);(MakeSizeis inApus.Engine.Types). Upgrader TODO rules added totools/engine5.upgrade.
Engine\onFrame(the command that runs one frame where the platform owns the main loop: Android, iOS) is renamed toEngine\RunFrame.- New notifications from the main window's frame:
Engine\Frame\Begin,Engine\Frame\BeforeRender(only for a rendered frame),Engine\Frame\End; the tag iswindow.frameNum. Apus.Engine.SteamAPIis back fromlegacy/, rewritten for Steamworks SDK 1.65. The library is loaded at run time (no link-time dependency, the game runs without it), callbacks are dispatched manually onEngine\Frame\Begin. API is theSteamrecord:InitSteamAPI→Steam.Init,DoneSteamAPI→Steam.Shutdown(called byTGameApplication),steamAvailable→Steam.available,steamID→Steam.userID,steamUserName→Steam.userName,steamGameLang→Steam.gameLanguage,SetSteamAchievement(name,enable)→Steam.SetAchievement/Steam.ClearAchievement+Steam.StoreStats. New:Steam.GetAchievement(account state, available right after Init),Steam.IsDlcInstalled,Steam.IsSubscribedApp,Steam.RestartAppIfNecessary, signalSteam\DlcInstalled,Steam.initResult/Steam.initError(why Init failed: no library, no client, client outdated). Removed:GetSteamAuthTicket(SDK 1.57+ requires the asynchronousGetAuthTicketForWebApifor Web API checks - add it when a server needs it) and the rawSteamAPI_*imports.
- The window lock is the outermost engine runtime lock (level 20; window surface data
25, the game object 30). The scene list is guarded by it:
TGameBase.AddScene,RemoveSceneandTopmostVisibleSceneno longer take the game lock. - The internal reentrant lock
TWindow.Lock(caller)/Unlockis renamed toLockState/UnlockState. It still gives no lifetime guarantee and does not switch thewindowcontext. - New cross-thread API of
TWindow:Lock:boolean/Unlock- worker entry: keeps the window alive, takes its lock and sets the thread'swindowcontext; false if the window is closing. Reentrant;Unlockrestores the previous context and does nothing without a matchingLock.QueueCall(proc,param)/QueueCall(method)- run a call on the window's thread at the start of its next frame, under the window lock. Every accepted call runs exactly once (at the latest while the window closes); false if closing.Acquire/Release- keep a reference to the window across threads.closing,ownerThreadID,IsOwnerThread; engine side:BeginClose,RunQueuedCalls,WaitReleased.
- The window thread holds the window lock on the frame segments that touch window state:
queued signals, input,
OnFrame/Process, scene and UI drawing. Subscribers ofEngine\Frame\BeginandEngine\Frame\BeforeRenderrun under it;Engine\Frame\End(after Present) runs outside it. Present,gfx.BeginPaintand sleeping are outside, so a worker'sLockwaits at most about one segment. onClickAsyncthreads (TUIButton,TUIToggleButton) start withwindowset to the clicked element's window (it was nil) andTUIElement.senderset to the element (it was not set). The window is kept alive until the handler returns, its lock is not held. A click on an element of a closing window does not start the handler.TUIElement.SafeDestroy->Removewith new semantics: the element and its subtree leave the tree at once (markeddeleted), the window's focus, mouse capture, modal and hotkey references into the subtree are dropped, and the memory is freed by the window's thread at the start of the next frame (right away if the element is not in a window). Callable from any thread; an element may remove itself in its own handler. Removing again does nothing; attaching a removed element is an ASSERT. DirectFreestays for the window's own thread outside handlers (ASSERT from another thread).TUIElement.Acquire/Releasekeep an element from being freed across threads (a held removed element waits);onClickAsyncholds its button, soonClickmay remove it.DeleteChildrennow removes the children this way.DestroyQueuedElementstakes the window.Detachbuilds a newchildrenarray, so loops over a snapshot (list:=children) survive removals made by the handlers they call.- Hint state is window state (
TWindow.hint):TUIHint.Current->TUIHint.Current(wnd);UI.CurrentHintreads the thread's window.ShowSimpleHintworks from any thread under the window lock of the parent's window (no parent: the thread's window); a parent outside any window shows nothing. The cursor position comes from the window, not from the calling thread. Each window has its own hint; the hint element is named_hint(not a unique name). - Keyboard focus is window state (
TWindow.focus, it was a threadvar ofApus.Engine.UITypes):SetFocus,HasFocusandIsActiveWindowuse the element's window,FocusedElementandSetFocusTo(nil)the thread's window. Focus set from any thread (the control thread at startup, a worker) is seen by the window. An element outside any window doesn't take the focus. NewClearFocus(wnd). underMouse,hooked,clipMouse,clipMouseRectand the cursor coordinates stay threadvars: mouse dispatch state of the window's thread, valid in that thread only. A mouse captor removed from another thread is dropped by the window's dispatch (DropRemovedMouseState). The open combo box drop-down is tracked per window thread (it was one global).- Each
TUIScenekeeps its own clock for element timers: a clock shared by all UI scenes of a thread stalled the timers of every scene processed after the first. - A UI element enters its parent's
childreninAfterConstruction, after the whole constructor chain (it was inserted by the base constructor, before the derived fields were set);UI\ItemCreatedis sent at that point too. A derived constructor still hasparentset but does not find itself inparent.children. - Scene effects: the engine draws an effect through
TSceneEffect.Paint(it calledDrawScenedirectly). A new effect created from inside the running effect'sDrawScene(a handler run by the scene'sProcess/Render) no longer frees it mid-draw: the replaced effect is freed whenDrawScenereturns. An effect that changes scene state hands it back inReplace(protected, virtual), not only in its destructor, so the new effect starts from the restored state;TShowWindowEffectdoes this and skips the rest of itsDrawSceneoncereplacedis set. - Scene shadows (
TGameScene.shadowColor): a scene draws only the part of its shadow not covered by the shadows of the scenes above it, so a stack of modal scenes with equal shadows dims the screen with one fill (the top one) instead of one per scene; the area under a scene is dimmed by the strongest shadow among it and the scenes above. Scenes of extra windows draw their shadows too (they were skipped). gfx.CopyFromBackbuffer(srcX,srcY,image): srcY now counts from the TOP of the surface (it counted from the bottom); a bound render target comes out top-down like the backbuffer (it came out bottom-up). The image is no longer flipped by movingdataand negatingpitch: an image can be read into repeatedly, and any pitch works.TWindow.ReadFrameRectis unchanged for callers; withTFrameSource.renderingand a presentation RT it now reads the right rows.- Removed: the unused global "under modal" shadow -
modalShadowColor(UIScene) and theUI\SetGlobalShadowsignal. - Removed:
FindWindowForScene(usescene.ownerWindow) andFindWindowForUIRoot(useelement.GetWindow, which resolves the window through the scene only). - Migration: calls of the old
window.Lock/Unlockin engine-level code becomeLockState/UnlockState; a worker thread that changes UI usesLock/Unlockand checks the result. No upgrader rule:Lock/Unlockare too common to rename blindly.
TDisplayMode.dmBorderlessremoved (it was an unfinished app-drawn frame: a WS_POPUP window, the manual resize inGameAppwas never implemented). The frame is a separate property now:TGameSettings.frame:TWindowFrame(System,Custom,CustomWhenMaximized), set at startup bywindowSetup.frame(replaceswindowSetup.borderless). A borderless window =dmWindow/dmFixedWindow+TWindowFrame.Custom.- With an app-drawn frame the window keeps the standard OS style: maximizing to the work
area of its monitor, Win+arrows, snap and the system menu keep working. The app tells
which part of the window is the caption through
TWindow.frameHitTest(TWindowArea: client, caption, edges); edges of a resizeable window resize it. TWindow.state:TWindowState(normal/minimized/maximized),TWindow.Maximize,TWindow.Restore.ENGINE\WINDOW\MINIMIZED/MAXIMIZED/RESTOREDare sent on every platform (they were SDL-only), on a state change only.- Windows: a window is centered on the work area of its own monitor (it was the primary one), a window maximized before switching to fullscreen is maximized again on return.
- SDL: the window is made borderless (
SDL_SetWindowBordered, also on maximize/restore forCustomWhenMaximized), the caption and edges come throughSDL_SetWindowHitTest, size limits throughSDL_SetWindowMinimumSize/MaximumSize. TUIWindow.BindToWindowmakes a UI window the frame of its OS window: it fills the scene, its own area in the topheaderunits is the caption (opaque children there stay clickable, transparent ones let the caption through),minW..maxHlimit the OS window (throughTWindow.sizeLimits). The drawer paints only the body: the caption row is up to the app.
- Video capture removed:
TGameBase/TGame/TWindow.StartVideoCap/FinishVideoCap,TFrameCapture.videoMode/videoPathand theVIDEOCAPTUREdefine (its unitApus.Engine.VideoCapturewas gone long ago). Use an external recorder. - TXT images removed:
LoadTXT,TImageFileType.ifTXTand theTXTIMAGESdefine (the path didn't compile and its fonts were never created).CheckImageFormatno longer reports text data as an image. GLintptr/GLsizeiptrinextra/dglOpenGL.pas/dglOpenGLES.pasareNativeInt(pointer-sized, as Khronos defines them; they were 32-bit).PGLcharin dglOpenGLES isPAnsiChar(the GLES renderer compiles with Delphi).
When the canvas is smaller than the render surface (fixed canvas in the sharp mode, the
matrix mechanism), the projection scales every 2D call up. Text used to be rasterized
in canvas pixels and stretched; now it is laid out and rasterized in real pixels.
Nothing changes when the canvas matches the surface (the ratio is 1).
IRenderTarget.pixelRatio- real pixels per virtual pixel of the current target.txt.Write*,Width,Height,Measure,MeasuredRect,LinkRectkeep working in canvas units; the font is rasterized at its size times the ratio.DrawTextFX: layer sizes (dx,dy,spread,blur) are in virtual pixels of the target, like the text coordinates (they used to be real pixels, which is the same at ratio 1). A scaled canvas bakes the sprite in real pixels.
TSurfaceConfig.canvasDPI(flexible canvas only): DPI of the canvas grid. 0 (default) = one canvas unit per display pixel, as before; N>0 = a unit is 1/N inch, the canvas is the display rect divided bydpi/Nand the projection scales it (matrix). Rejected with a fixed or half-fixed canvas.TGameApplication.SetupLogicalCanvas(dpi=96)- preset: the canvas follows the client area in logical pixels, so the same numbers give the same physical size on any display.game.screenScalestays 1 there (the projection does the scaling), like over a fixed canvas.
FPC compiles ASSERT out unless -Sa is given, and no engine build had it. build.cfg
and every test/smoke script now pass -Sa. A project that wants a build without
assertions adds -Sa- to its own options.
When the projection scales the canvas (pixelRatio>1), a virtual pixel is a cell of real
pixels. Fills and images cover whole cells; what is thinner than a cell places itself on
real pixels. Nothing changes at ratio 1 (checked by tests/TestPixelGrid).
draw.Line: a hairline at coordinate c lies on the first real pixel of the cell.draw.Rect: the frame lies on the outer real pixels of its cells, so it matchesFillRectwith the same corners. Drawn as one-pixel strips instead of a line loop (not betweenBeginLines/EndLines).draw.RRectwithout width: moved outwards the same way.draw.RRectwith width: its outer edge is the edge ofFillRRectwith the same arguments (the radius was 0.4 larger and the arc had fewer segments, so the fill showed outside the frame at the corners).- Rounded corners: the default number of arc segments follows the radius in real pixels;
an explicit
stepsis clamped to 1..16 (a larger one used to overrun a stack buffer). draw.ShadedRect: each depth step is one virtual pixel wide at any ratio (drawn with bands instead of lines); the same pixels at ratio 1.
game.defaultFont, smallFont and largerFont grow in proportion to the canvas DPI
(7.376*dpi/96), like Dp() and a logical canvas. They used to grow slower
(2+0.056*dpi): the same size at 96 DPI, 14% smaller at 192 DPI, so layouts built
with Dp() had too much room for the text.
The projection flips Y for the backbuffer but not for a texture target, which mirrors
the winding, while the GL front face was fixed at CCW: TCullMode.DrawCCW kept the back
faces in a render target (3D into an RT, a frame through the presentation RT, scene
transition effects). The GL backend now switches the front face with the target, so a
TCullMode keeps the same faces everywhere (tests/TestCulling). Code that compensated
for it (e.g. DrawCW for 3D into an RT) has to go back to the normal mode.
TPlaySettings.loopStart/loopEndaredoubleseconds of the media file,loopEnd=0= the end of the file (weresinglewith no stated unit and ignored by the SDL backend). Seconds stay valid when a file is resampled; double keeps a sample position exact.- Music entries of the sound config take the loop start in one of two keys:
loopTimein seconds, orloopPosin samples of the file - the unit of the original Engine 2/IMX configs, which therefore work unchanged (verified on Spectromancer tracks: the audio beforeloopPosmatches the end of the track at offset 0).loopTimewins when both are given. - SDL backend: a looped track with a loop region is decoded whole once and played
on a reserved mixing channel (the loop is sample-exact); other music still
streams through
Mix_Music. One music stream at a time as before.
- The game-side sound API is the
ISoundSysteminterface, the globalsound(always assigned: without a backend or beforeInitthe calls do nothing):InitSoundSystem/DoneSoundSystem/IsMusicPlaying->sound.Init/Done/IsMusicPlaying. Besides them it has direct calls for what theSound\...events do (Play,PlayMusic/StopMusic,PlayFile/StopFile/StopGroup/SetGroup,ClearCache,SetVolume,Pause), procedural sound (PlayStream) and control of a playback by its handle:SetChannelVolume/Pan/Speed(with an optional slide time),StopChannel,IsPlaying. The events stay as they were. - Calls from any thread return at once: they are queued to the sound thread, and a playback handle is given out before the playback starts.
TSoundStreamProcgenerates a procedural sound in the audio thread (float samples, the rate is passed in); the contract is in its comment.- SDL backend: samples, streams and looped music play through the backend's own
mixing (a channel effect over a silent carrier), so samples get pitch: the
speed/freqof the config and ofSound\Play, which were ignored by the SDL backend before, now apply - data withspeed<>1 sounds the way it did in Engine 2. At speed 1 a sample plays bit-exact as before. PlayMusictransitions are three times in seconds (fade-out, start delay, fade-in); the tag presets 0..6 ofSound\PlayMusicmap onto them. The start position (fromPos, the high bits of the tag,Sound\MusicPos) was stored but never used - now the track starts from it.PlayMusic\None(or a missing track) also cancels a track that waits for its start.Sound\Change\...(a stub that did nothing) is removed.
- New
Apus.Config(manual/config_files.md): documents in the CTL3 syntax (braced sections, arrays, inline comments); CTL2 files and a JSON subset are read by the same parser. The engine reads its configs through it;Apus.ControlFilesis still in Base, but nothing in the engine opens files with it any more. Apus.Engine.GameApp: the globalappConfig:TConfigis the document ofappSetup.configFile:Prepareopens it,SaveOptionssaves it, it is freed with the application (nil without a config file). AfterPrepare,appSetup.configFileholds the path of the file used (the copy inAppDataDirwhen the shipped one must stay intact), no longer the bare name used as a control file key. The keys are unchanged and case-insensitive:appConfig.Root.Int('Options.WindowWidth',1024).- Config problems are not fatal, but noticeable.
TConfig.Createnever raises on the file: a missing or broken file gives an empty document (the defaults),LoadErrorand a report; such a file is not overwritten bySaveand is picked up byReloadIfChangedonce fixed. Every problem (a failed load, reload or save, a value that does not convert, a write the content does not allow) is logged and passed toonConfigProblem; GameApp shows it as a toast.ReloadandSaveare functions returning success. SetFloat(path,value,digits=5)writes 5 significant digits by default;digits=0gives the shortest form that reads back exactly.Apus.Engine.GameApp: a missingappSetup.configFileis no longer aFatalError: the defaults are used, a toast says so, andSaveOptionscreates the file.Apus.Engine.Sound: an event may also be a section,sampleQuiet = { file = "sample.ogg", vol = 30 };PreloadSamplesmay be an array of files; a missingPreloadSamplesmeansNONE(it was a config loading error). A broken event is reported and skipped; a config error no longer fails the sound initialization.Apus.SCGI: readsconfig.ctlthroughApus.Config; the file is unchanged.- Data files: CTL2 files keep working. A CTL2 file that the program saves is written
in the CTL3 syntax. Duplicate keys in a file are an error now (CTL2 silently kept
the last one);
$Includemerges the file into the including section (in CTL2 the included file was a separate namespace). - Migration of game code:
ctlGetInt(appSetup.configFile+':\Options\X',d)→appConfig.Root.Int('Options.X',d); other files:cfg:=TConfig.Create(name)andcfg.Root.Str('A.B',...). The full table is section 13 of the manual chapter; upgrader TODO rules are intools/engine5.upgrade. The demo configs use the CTL3 syntax now.
- The OS close button, Alt+F4 and the OS quit command (SDL
SDL_QUIT, e.g. Cmd+Q) no longer close the window at once: they callTWindow.RequestClose, which sendsENGINE\WINDOW\CLOSEREQUEST. A handler that wants to ask the user callswindow.ClosePendingright away (instant handler, window's thread) and the window stays; the app later closes it withwindow.Terminate(main window: asEngine\Cmd\Exit). WithoutClosePendingthe window closes as before. - The app's own close button should call
window.RequestClosetoo, so it asks the same way;Terminate/Engine\Cmd\Exitclose without asking. ENGINE\WINDOW\CLOSE(SDL only, sent when the window was already closing) is removed: useENGINE\WINDOW\CLOSEREQUEST.