Repository navigation
Expand file tree
/
Copy pathApus.Engine.Window.pas
More file actions
2716 lines (2498 loc) · 86.3 KB
/
Copy pathApus.Engine.Window.pas
File metadata and controls
2716 lines (2498 loc) · 86.3 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
// Render window and backbuffer management.
//
// TWindow owns a backbuffer (size, format, clear color) and
// the scene stack rendered into it. Handles per-window frame
// timing, frame capture, and debug overlay state.
// Platform-level window creation is handled by ISystemPlatform;
// this unit operates above that layer.
//
// Copyright (C) Ivan Polyacov, Apus Software (ivan@apus-software.com)
// This file is licensed under the terms of BSD-3 license (see license.txt)
// This file is a part of the Apus Game Engine (http://apus-software.com/engine/)
{$I defines.inc}
unit Apus.Engine.Window;
interface
uses Apus.Core, Apus.Geom2D, Apus.Engine.Types, Apus.Engine.Scene, Apus.Threads,
Apus.Classes, Apus.Images, Apus.Engine.Resources, Apus.Engine.Keys;
const
FRAME_TIME_RING_SIZE=512;
type
TWindow=class;
TSceneArray=array of TGameScene;
TWindowArray=array of TWindow;
TRenderProc=procedure of object;
// Deferred call run on a window's own thread (see TWindow.QueueCall)
TWindowCall=procedure(param:pointer);
TWindowMethod=procedure of object;
TWindowQueuedCall=record
call:TWindowCall; // either call (with param)...
method:TWindowMethod; // ...or method
param:pointer;
end;
TWindowQueuedCalls=array of TWindowQueuedCall;
{$SCOPEDENUMS ON}
TWindowState=(Normal,Minimized,Maximized); // state of a window in the window modes
{$SCOPEDENUMS OFF}
// App-drawn frame: part of the window at a canvas point (see TWindow.frameHitTest)
TWindowHitTest=function(x,y:integer):TWindowArea of object;
// Limits of the client size the user can resize the window to, canvas units: 0 - no limit
TWindowSizeLimits=procedure(out minSize,maxSize:TSize) of object;
TFrameCapture=record
singleFrame:boolean; // request frame capture
// 0 - keep in data, 2 - save as JPEG, 3 - save as PNG
target:integer;
data:TObject;
capturedName:string;
capturedTime:int64;
procedure Reset;
// Requests: capture the next presented frame (under the window lock)
procedure RequestScreenshot(saveAsJpeg:boolean); // save to Screenshots\ and copy to the clipboard
procedure RequestFrame(obj:TObject); // obj<>nil: send the image with Engine\FrameCaptured
// Serve the request: the window's thread, after the frame is presented
procedure Capture(wnd:TWindow);
end;
// Per-frame diagnostic log: what the frame has done so far (saved on a crash)
TFrameLog=record
current:string; // this frame
previous:string; // the frame before
procedure Add(const st:string);
procedure NextFrame; // current becomes previous
end;
// Per-frame render call counters (reset each frame in PresentRenderedFrame).
TRenderStats=record
drawCalls:integer;
shaderChanges:integer;
texChanges:integer;
clipChanges:integer;
verticesDrawn:integer;
procedure Reset;
end;
TFrameTiming=record
// Robot diagnostics toggle.
phaseMetrics:boolean;
// Per-frame phase timings (microseconds).
pendingMsgUs:integer;
lastMsgUs:integer;
lastOnFrameUs:integer;
lastRenderUs:integer;
lastPresentUs:integer;
lastSleepUs:integer;
// Frame duration ring and per-phase history.
frameTimeRing:array[0..FRAME_TIME_RING_SIZE-1] of integer;
phaseMsgRing:array[0..FRAME_TIME_RING_SIZE-1] of integer;
phaseOnFrameRing:array[0..FRAME_TIME_RING_SIZE-1] of integer;
phaseRenderRing:array[0..FRAME_TIME_RING_SIZE-1] of integer;
phasePresentRing:array[0..FRAME_TIME_RING_SIZE-1] of integer;
phaseSleepRing:array[0..FRAME_TIME_RING_SIZE-1] of integer;
frameTimeRingPos:integer;
frameTimeRingCount:integer;
lastFrameTimeUs:integer;
// High-precision frame timer used by render loop.
frameTimer:int64;
frameTimerReady:boolean;
// Cached update moments for throttled FPS refresh.
lastFpsUpdate:int64;
lastSmoothFpsUpdate:int64;
// Accumulated frame time while redraw is skipped (for lazy redraw fallback).
idleRedrawAccUs:integer;
// Accumulated frame time since last presented frame (for FPS sampling).
presentSampleAccUs:integer;
fps,smoothFps:single; // current and smoothed FPS (see UpdateFps)
procedure Reset;
procedure PushSample(deltaUs,msgUs,onFrameUs,renderUs,presentUs,sleepUs:integer);
function MaxRecentFrameUs(sampleCount:integer):integer;
function CalcTrimmedFrameMs(windowUs,minFrames,trimPermille:integer; out avgMs:double):boolean;
procedure UpdateFps;
end;
// Per-window modal dialog state: a stack of modal UI roots.
// Fields are typed TObject because TWindow's interface section cannot see
// TUIElement (Apus.Engine.UITypes uses this unit). Typed, behavior-rich
// access is provided by TModalStateHelper (record helper) in Apus.Engine.UITypes.
TModalState=record
element:TObject; // active modal UI root (TUIElement), nil if none
stack:array[1..8] of TObject; // suspended modal roots below the active one
stackSize:integer;
end;
// Hint state of a window (typed access: TUIHint.Current, ShowSimpleHint)
TWindowHintState=record
element:TObject; // hint (TUIHint) shown by the last ShowSimpleHint, nil if none
area:TRect; // leaving this area (canvas coordinates) hides the hint
fastUntil:int64; // until then element hints pop up faster (right after one was shown)
showTime:int64; // when the hint of the element under the mouse is due, 0 - none
lastText:String8; // hint text of the element under the mouse in the previous frame
end;
// Keyboard focus of a window (typed access: FocusedElement, TUIElement.SetFocus/HasFocus)
TWindowFocusState=record
element:TObject; // element with keyboard focus (TUIElement), nil if none
activeWnd:TObject; // TUIWindow that holds the focus, nil if the focus is outside any
end;
// Surface rebuild requests: stored by any thread, taken by the window's own thread
// (see TWindow.ApplyPendingSurface)
TSurfacePending=record
lock:TLock; // a leaf lock: nothing is entered inside
mask:integer; // see psmXXX constants
client:TSize;
dpi:integer;
insets:TRect;
procedure SetClient(const size:TSize);
procedure SetDPI(value:integer);
procedure SetInsets(const r:TRect);
procedure Force; // full rebuild
function IsEmpty:boolean;
// Move the requested values into input and clear the requests; returns the mask taken
function Take(var input:TSurfaceInput):integer;
end;
// Presentation render target of a window (see TWindow.EnableDefaultRT)
TDefaultRT=record
allowed:boolean; // the RT may be created for this window
zbuffer:byte; // parameters remembered for a deferred/repeated creation
depthTex:boolean;
rt:TTexture; // nil - rendering goes straight to the backbuffer
depth:TTexture; // depth buffer texture (with depthTex)
end;
// Calls queued for the window's thread (see TWindow.QueueCall)
TWindowCallQueue=record
lock:TLock; // a leaf lock: nothing is entered inside
items:TWindowQueuedCalls;
count:integer; // length of items, readable without the lock
closed:boolean; // no calls are accepted any more
function Add(const c:TWindowQueuedCall):boolean; // false once closed
function TakeAll:TWindowQueuedCalls;
procedure Close;
end;
// Input frame: the input published to one pass of TWindow.OnFrame. Button transitions
// are gathered from the events between passes and published at the start of the next.
TInputFrame=record
pendingPressed,pendingReleased:byte; // button transitions since the last publication
pressed,released:byte; // button transitions of the published frame
pos,prevPos:TPoint; // pointer at this and the previous publication
outside,prevOutside:boolean;
num:int64; // publications so far
procedure AddTransition(bit:byte;down:boolean);
end;
// Set of scancodes (Scan.XXX)
TScanSet=record
bits:array[0..Scan.Count div 32-1] of cardinal;
procedure Clear;
procedure Put(code:integer;value:boolean);
function Has(code:integer):boolean;
end;
// --- Virtual input (see TVirtualInput) ---
TVirtualInputOpKind=(
vioMode, // switch the input mode (enable)
vioReset, // cancel the gesture: drop the capture, release the buttons without a click and the keys
vioMove, // move the pointer (pos)
vioButton, // press or release a button (button, pressed) at the current position
vioWheel, // turn the wheel (delta) at the current position
vioKey, // press or release a key (key, scanCode, pressed)
vioChar // enter a character (ch)
);
TVirtualInputOp=record
kind:TVirtualInputOpKind;
enable:boolean; // vioMode: true - virtual input, false - back to the physical one
pos:TPoint; // vioMove: target point
clientSpace:boolean; // vioMove: pos is in pixels of the client area, not in the canvas space
button:byte; // vioButton: 1 - left, 2 - right, 3 - middle
pressed:boolean; // vioButton, vioKey: true - down, false - up
delta:integer; // vioWheel: 120 per notch, positive - away from the user (as MOUSE\SCROLL)
key:TKey; // vioKey: the key event's TKey...
scanCode:word; // ...and its position (Scan.XXX)
ch:word; // vioChar: UTF-16 code unit (as KBD\UNICHAR)
delayMs:integer; // applied not earlier than delayMs after the previous operation
ticket:int64; // assigned by TVirtualInput.Queue
end;
TVirtualInputTicket=(vitPending,vitDone,vitDropped);
TVirtualInputTicketRange=record
lo,hi:int64;
end;
// Input state applied by the window's thread
TVirtualInputState=record
active:boolean; // virtual mode is on
pos:TPoint; // canvas space; ($3FFF,$3FFF) - outside the canvas
buttons:byte; // mbLeft, mbRight, mbMiddle
under:String8; // UI element under the pointer: name ('' - none or unnamed)...
underClass:String8; // ...and class ('' - none)
keys:TScanSet; // keys held
focus:String8; // UI element with the keyboard focus: name...
focusClass:String8; // ...and class ('' - none)
frame:integer; // window frame of the last applied operation
end;
// Virtual input of a window (TWindow.virtualInput): pointer and keyboard input that does
// not come from the OS. Operations are queued from any thread; the window's own thread
// applies them in order, one per frame, through the regular routing: mouse - hit-test,
// hover, capture, clicks; keys and characters - the KBD\ events of a platform. While the
// virtual mode is on the window ignores the physical pointer, buttons and keyboard.
// The object is reference counted and outlives its window: a holder (e.g. a pending
// Robot API request) never touches the window, and once the window closes every
// operation not applied yet is dropped and new ones are refused.
TVirtualInput=class
private
lock:TLock; // a leaf lock: nothing is entered inside
refCount:integer;
ownerPtr:pointer; // identity only: the window may be gone
ownerName:String8;
ops:array of TVirtualInputOp;
opCount:integer;
issuedTicket:int64; // last ticket issued
takenTicket:int64; // operation being applied by the window thread (0 - none)
doneTicket:int64; // every ticket <= doneTicket is done or dropped
dropped:array of TVirtualInputTicketRange; // tickets dropped unapplied
reqActive:boolean;
reqButtons:byte;
reqKeys:TScanSet;
lastTakeMs:int64; // when the last operation was taken (for delayMs)
activeValue:boolean;
closedValue:boolean;
applied:TVirtualInputState;
procedure DropQueued; // under lock
procedure UpdateDone; // under lock
public
constructor Create(owner:TWindow);
procedure AddRef;
procedure Release; // the last reference frees the object
// --- Producer side (any thread) ---
// Returns the ticket of the operation (>0), 0 if the window is closed
function Queue(op:TVirtualInputOp):int64;
function TicketState(ticket:int64):TVirtualInputTicket;
// Drop every queued operation not taken by the window yet (their tickets become dropped)
procedure Cancel;
// Mode, buttons and keys as they will be once every queued operation is applied
function RequestedActive:boolean;
function RequestedButtons:byte;
function RequestedKeys:TScanSet;
function QueuedCount:integer; // operations not completed yet
function LastTicket:int64; // ticket of the last queued operation (0 - none yet)
function State:TVirtualInputState; // last state published by the window's thread
function IsActive:boolean; inline; // virtual mode is on (as applied by the window)
function IsClosed:boolean;
function IsMainWindow:boolean; // owned by mainWindow
property windowName:String8 read ownerName;
// --- Window side (the window's own thread) ---
// The next operation, if its delay has passed by nowMs
function TakeNext(out op:TVirtualInputOp;nowMs:int64):boolean;
procedure Complete(const ticket:int64;const st:TVirtualInputState);
procedure SetActive(enable:boolean);
procedure Close; // the window closes: drop the queue, refuse new operations
end;
// Base class for engine windows.
// Platform-specific subclasses implement abstract methods.
// Created via ISystemPlatform.CreateWindow.
TWindow=class(TNamedObject)
protected
class function ClassHash:pointer; override;
private
// Working surface: accumulated resolver input and cross-thread rebuild requests.
surfaceInput:TSurfaceInput; // client size, DPI, native insets, render scale
pending:TSurfacePending;
defaultRT:TDefaultRT;
// Per-frame timing: high-precision microseconds (ms values are derived)
frameStartUsValue:int64;
frameDeltaUsValue:int64;
// Lifetime and cross-thread entry (see Acquire, Lock, QueueCall, BeginClose)
ownerThread:TThreadIdent;
usageCount:integer; // references taken by Acquire
closingValue:integer; // 1 once BeginClose was called
callQueue:TWindowCallQueue;
runtimeLock:TLock; // the window state lock (see LockState)
input:TInputFrame;
pointerOutside:boolean; // the pointer is not over the canvas (mousePos may be clamped to its edge)
inFrame:boolean; // an OnFrame pass is running
function GetClosing:boolean;
function GetFrameStartMs:int64; inline;
function GetFrameDeltaMs:int64; inline;
procedure SetupViewport; // apply the current surface to the graphics backend
procedure EnsureDefaultRT; // create the default RT if the surface needs one
procedure UpdateSurfaceRT; // keep the default RT in sync with surface.renderSize
procedure UpdateScreenScale; // DPI -> uiScale ladder (main window only)
procedure ApplyVirtualInputOp(const op:TVirtualInputOp);
procedure CancelMouseGesture; // drop the capture, release the buttons without a click
procedure ReleaseKeys; // every held key gets KBD\KEYUP
function KeysShiftState:byte; // shiftState from the held keys
procedure DeliverMouseButton(btn:byte;pressed:boolean); // update mouseButtons + dispatch
// Pointer at a canvas position; off the canvas it is outside: the off-canvas sentinel,
// or clamped to the canvas edge while a button is held (a drag never sticks)
procedure PlacePointer(const canvasPos:TPoint;onCanvas:boolean);
procedure PublishInputFrame; // start of an OnFrame pass: publish the input gathered since the last one
procedure NotifyScenesResize; // the surface changed: TGameScene.onResize of every scene
procedure DeliverMouseWheel(delta:integer); // MOUSE\SCROLL + dispatch
function VirtualInputSnapshot:TVirtualInputState;
public
// Working surface (R-31): declared axes + resolved snapshot.
config:TSurfaceConfig; // runtime authority for this window (a copy of TGameSettings.surface)
surface:TSurfaceState; // published snapshot; written only by this window's own thread between frames
presentedGeneration:integer; // surface generation of the last presented frame (see SurfaceSettled)
screenChanged:boolean; // set to true to request frame rendering
// Input state snapshot for this window: values are stored per-window (not global)
// so they stay stable during frame processing even with multiple window threads.
mousePos:TPoint; // mouse position in game coordinates
oldMousePos:TPoint; // previous mouse position
// UI-dispatch bookkeeping (kept next to mousePos as it's positional state):
moveKind:TMoveKind; // set by the UI dispatcher before each gameplay onMouseMove call
mouseOverUI:boolean; // true while the cursor sits over a consuming UI element (prev-frame state)
mouseButtons:byte; // buttons held now (bit 0=left, 1=right, 2=middle), set by the button events
// Virtual pointer input of this window (see TVirtualInput). Never nil; owned by the
// window (holders elsewhere AddRef it). While it is active the physical pointer and
// buttons are ignored in this window.
virtualInput:TVirtualInput;
shiftState:byte; // shift keys: aggregate (sscBaseMask) + right-side bits (sscRightMask)
// Keys by scancode (Scan.XXX): bit 0 - down now; bit 1 - went down, bit 2 - went up in
// the published frame (a press shorter than a frame gives both); bits 3, 4 - the same
// since the publication (internal). Changed by KeyTransition.
keyState:array[0..Scan.Count-1] of byte;
keyCodes:array[0..Scan.Count-1] of TKey; // TKey of the held keys
// Files dropped onto the window from a file manager: the last drop, valid until the
// next one. Set by the window's thread right before ENGINE\WINDOW\DROPFILES (tag = count).
droppedFiles:Strings8;
dropPos:TPoint; // drop point in canvas coordinates; (-1,-1) if the platform does not report it
// Window state
active:boolean; // true when window is visible and updated
state:TWindowState; // set by the platform, changes send ENGINE\WINDOW\MINIMIZED/MAXIMIZED/RESTORED
frame:TWindowFrame; // frame in effect, set by Configure: System in fullscreen
closeDeferred:boolean; // set by ClosePending during RequestClose
// App-drawn frame (frame<>System): which part of the window is at a canvas point - caption
// (drags the window), edges or client. nil - only the edges resize the window. Called on the
// window's thread at the mouse polling rate: must be fast. It may take the state lock briefly
// (TUIWindow does), so the lock must not be held while waiting for the window's thread.
// Set under LockState (see TUIWindow.BindToWindow)
frameHitTest:TWindowHitTest;
// Client size limits for the window modes, asked when the OS needs them (on the window's
// thread). nil - no limits. Set under LockState
sizeLimits:TWindowSizeLimits;
frameNum:integer; // increments every frame
frameLog:TFrameLog; // per-frame diagnostic log (cleared every frame, saved on error)
// Per-window data
renderThread:IThread; // nil for main window, set by AddWindow for extra windows
timings:TFrameTiming;
stats:TRenderStats;
capture:TFrameCapture;
scenes:TSceneArray;
topmostScene:TGameScene; // last topmost active scene for this window
modal:TModalState; // modal dialog state for this window (see TModalStateHelper)
hint:TWindowHintState; // hint state for this window (see TUIHint.Current)
focus:TWindowFocusState; // keyboard focus in this window (see FocusedElement)
deletedUI:array of TObject; // UI elements deleted in this window, freed at frame start (TUIElement.Remove)
// Frame timing (per-window, read-only from outside): `*Ms` are derived from `*Us`
property frameStartUs:int64 read frameStartUsValue;
property frameDeltaUs:int64 read frameDeltaUsValue;
property frameStartMs:int64 read GetFrameStartMs;
property frameDeltaMs:int64 read GetFrameDeltaMs;
// Surface shortcuts (read the published snapshot)
property canvasWidth:integer read surface.canvasSize.cx; // draw/input space
property canvasHeight:integer read surface.canvasSize.cy;
property clientWidth:integer read surface.clientSize.cx; // native client area
property clientHeight:integer read surface.clientSize.cy;
property displayRect:TRect read surface.displayRect; // picture placement in the client area
// One canvas unit is not one pixel: a fixed canvas is stretched into the render
// surface, so it carries the DPI scaling by itself. Everything the engine sizes
// in canvas units (fonts, overlays, Dp) must be measured with this DPI, not with
// surface.dpi - otherwise the DPI factor is applied twice.
function canvasDPI:single; // dots per inch of one canvas unit
// windowName - ASCII identifier: letters, digits, '_', '-', '.' (the main window is 'Main')
constructor Create(const windowName:String8);
destructor Destroy; override;
procedure SetFrameTiming(startUs,deltaUs:int64);
procedure ResetFrameTiming;
// Window state lock (scene list, UI tree, window UI state): reentrant, no lifetime
// guarantee, no context switch. Engine code; worker threads use Lock/Unlock.
procedure LockState(caller:pointer=nil);
procedure UnlockState;
// Thread that runs this window's frames - the one that created it (main window too)
property ownerThreadID:TThreadIdent read ownerThread;
function IsOwnerThread:boolean;
// Set once the window starts closing; never cleared. From then on Acquire, Lock and
// QueueCall fail.
property closing:boolean read GetClosing;
// Keep a reference to the window across time or threads: false once it is closing.
// A raw window pointer kept by another thread without Acquire is unsafe.
function Acquire:boolean;
procedure Release;
// Worker entry: Acquire + LockState + this thread's `window` context. Returns false
// (holding nothing) if the window is closing. Reentrant. Unlock restores the previous
// context; an Unlock without a matching successful Lock in this thread does nothing.
function Lock:boolean;
procedure Unlock;
// Run the call on this window's thread at the start of its next frame, under the
// window lock. Any thread. False if the window is closing: the call will not run and
// param stays with the caller. Every accepted call runs exactly once - at a frame
// start or, at the latest, while the window closes (check `closing` there).
function QueueCall(call:TWindowCall;param:pointer=nil):boolean; overload;
function QueueCall(method:TWindowMethod):boolean; overload;
// Engine side of the above
procedure RunQueuedCalls; // window's thread at frame start; the closer after its thread stopped
procedure BeginClose; // first step of closing: refuse new Acquire/Lock/QueueCall
function WaitReleased(timeoutMs:integer):boolean; // all Acquire references released?
procedure ResetSceneData;
procedure AddScene(scene:TGameScene);
function RemoveScene(scene:TGameScene):boolean;
function TopmostVisibleScene(fullScreenOnly:boolean=false):TGameScene;
// Called once per frame (after ProcessMessages) to update mouse position,
// emit MOUSE\MOVE signal and notify scenes. Buttons are handled immediately.
procedure FlushMouseInput;
// Mouse input of the platform: a physical button/wheel event (window's thread). Samples
// the pointer (unless the caller has set it: touch), updates mouseButtons, sends
// MOUSE\BTNDOWN/BTNUP/SCROLL and dispatches the event; dropped while the virtual mouse
// is active.
procedure PlatformMouseButton(btn:byte;pressed:boolean;sample:boolean=true);
procedure PlatformMouseWheel(delta:integer);
// The platform lost the pointer (the OS capture taken away, focus lost, the window
// hidden): a physical gesture in progress ends without a click, the held buttons are
// released (Mouse.WasReleased). Ignored while the virtual mouse is active.
procedure PlatformCancelGesture;
// The platform lost the keyboard (focus lost, the window hidden): every held key is
// released through the regular path (KBD\KEYUP), as if the user let it go. Ignored while
// the virtual input is active.
procedure PlatformReleaseKeys;
// shiftState of the frame: from the OS, or from the held keys while the virtual input is
// active (window's thread)
procedure UpdateShiftState;
// A key event (window's thread): updates keyState. False for a release of a key that is
// not down (pressed before the window got the focus): such a release is dropped, the
// receivers get down/up pairs. Scancode 0 (unknown key) is not tracked.
function KeyTransition(scanCode:integer;key:TKey;pressed:boolean):boolean;
// Files dropped by the platform (window's thread): fills droppedFiles/dropPos and
// sends ENGINE\WINDOW\DROPFILES
procedure PlatformDropFiles(const files:Strings8;const clientPos:TPoint;hasPos:boolean);
// Window state reported by the platform (window's thread): sends ENGINE\WINDOW\MINIMIZED,
// MAXIMIZED or RESTORED (back to normal) when it changes
procedure PlatformStateChanged(newState:TWindowState);
// Request to close the window (window's thread): the OS close button, Alt+F4, quit command
// of the OS, or the app's own close button. Sends ENGINE\WINDOW\CLOSEREQUEST (window - the
// window being closed) and closes the window (Terminate) unless a handler called ClosePending.
// The handler must run in the window's thread right away (emInstant): a queued one is too late
procedure RequestClose;
// Called by a ENGINE\WINDOW\CLOSEREQUEST handler: the app asks the user first, the window
// stays open. The app then closes it with Terminate or keeps it. Valid for one request only
procedure ClosePending;
// Close without asking: the main window stops the app (as Engine\Cmd\Exit), an extra window
// stops its loop
procedure Terminate; virtual; abstract;
// Pointer position from the platform (window's thread). SetPointer maps client pixels
// to the canvas: outside the display rect the pointer is outside the canvas - the
// off-canvas sentinel, or clamped to the canvas edge while a button is held, so a drag
// started inside never sticks. SetPointerOutside - the pointer is not over this window.
procedure SetPointer(const clientPos:TPoint);
procedure SetPointerOutside;
// Mouse step of a frame, window's thread under the window lock. Virtual mode: applies
// the next queued virtual mouse operation, otherwise samples the OS pointer (if
// physicalPointer). Then dispatches the move (FlushMouseInput) - in the physical mode
// only when the pointer was sampled.
procedure FrameInput(physicalPointer:boolean);
function ProcessScenes(deltaTime:integer):boolean;
// --- Working surface: rebuild requests (any thread; only stores the request) ---
procedure RequestResize(const client:TSize); overload;
procedure RequestResize(width,height:integer); overload;
procedure RequestDPI(dpi:integer);
procedure SetNativeSafeArea(const insets:TRect);
procedure SetRenderScale(scale:single);
procedure SetSurfaceConfig(const cfg:TSurfaceConfig); // validates, then requests a rebuild
procedure InvalidateSurface; // force a full rebuild on the next ApplyPendingSurface
// True when no surface change is waiting and a frame for the current surface
// generation has already been presented. Frame readback (screenshot, pixel)
// must wait for this: right after a resize the presented frame is still the old one.
function SurfaceSettled:boolean;
// Apply a pending request. Called by the window's OWN thread at frame start:
// hook -> validate -> resolve -> RT/viewport -> publish snapshot -> notifications.
procedure ApplyPendingSurface;
procedure Close; virtual; abstract;
// Apply display/window settings (mode, size, style, position) to an existing native window.
// Called on startup and on runtime display-mode changes (Alt+Enter etc). Should also trigger resize flow so engine can recompute render/display areas.
procedure Configure(params:TGameSettings); virtual; abstract;
procedure Show(show:boolean); virtual; abstract;
function GetHandle:THandle; virtual; abstract;
procedure GetSize(out width,height:integer); virtual; abstract;
procedure MoveTo(x,y:integer;width:integer=0;height:integer=0); virtual; abstract;
procedure SetCaption(text:string); virtual; abstract;
procedure Minimize; virtual; abstract;
procedure Maximize; virtual; abstract;
procedure Restore; virtual; abstract; // back to normal from the minimized or maximized state
// The app draws the caption now: frame is Custom, or CustomWhenMaximized and the window is maximized
function CustomFrameShown:boolean;
procedure FlashWindow(count:integer); virtual; abstract;
procedure ProcessMessages; virtual; abstract;
// True when native close/quit was requested for this window (used to stop main loop gracefully).
function IsTerminated:boolean; virtual; abstract;
procedure ScreenToClient(var p:TPoint); virtual; abstract;
procedure ClientToScreen(var p:TPoint); virtual; abstract;
// Sample the OS pointer position for this window and update mousePos
// in game coordinates. Called once per frame from FrameLoop before
// FlushMouseInput. Does nothing while the virtual mouse is active.
procedure SamplePointer;
// Platform part of SamplePointer: reports the pointer through SetPointer/SetPointerOutside
procedure SampleOSPointer; virtual; abstract;
// Graphics backend lifecycle for this window
// Create/activate graphics context and initialize backend-facing window surface state.
procedure InitGraph; virtual; abstract;
// Create graphics context that shares resources (textures, buffers, shaders) with primary window.
procedure InitGraphShared(primary:TWindow;mainContextReleased:boolean=false); virtual; abstract;
// Release graphics context and backend-facing window surface resources.
procedure DoneGraph; virtual; abstract;
// Temporarily detach/attach current graphics context (used for shared-context startup sequencing).
procedure ReleaseGraphContext; virtual;
procedure ActivateGraphContext; virtual;
procedure PresentFrame; virtual; abstract;
function SetVSync(divider:integer):boolean; virtual; abstract;
// Frame processing and scene rendering. The window thread holds the window lock only on
// the frame segments that touch window state: input dispatch, queued signals, OnFrame
// (takes it itself), rendering (RenderFrame takes it around the scene parts, leaving
// gfx.BeginPaint outside: with vsync the driver may block there; the caller of
// RenderScenes holds it). Present, sleeping and the OS message pump run outside it.
function OnFrame:boolean;
procedure RenderFrame(const params:TGameSettings;
drawCursor,drawOverlays:TRenderProc);
procedure RenderScenes(drawOverlays:TRenderProc);
// Coordinate transforms between client area and canvas (draw/input) space
function ClientToCanvas(const p:TPoint):TPoint;
function CanvasToClient(const p:TPoint):TPoint;
// False when the point is outside displayRect (canvasP is still computed)
function TryClientToCanvas(const p:TPoint;out canvasP:TPoint):boolean;
// Default render target
// Allow a presentation RT for this window and create it if the surface needs one.
// Without it rendering goes straight to the backbuffer.
procedure EnableDefaultRT(zbuffer:byte;useDepthTex:boolean);
procedure InitDefaultRenderTarget(width,height:integer; zbuffer:byte; useDepthTex:boolean);
function GetDepthBufferTex:TTexture;
// Blit default RT to backbuffer (called before platform PresentFrame)
procedure BlitDefaultRT(tintColor:cardinal);
// Full present cycle: blit the default RT + swap + update counters
procedure PresentRenderedFrame(tintColor:cardinal);
// Frame readback: canvas space <-> pixels of the surface holding the frame.
// The backbuffer is read in physical pixels, so a canvas-space region must be
// mapped first - canvasSize and clientSize differ whenever the canvas is fixed,
// scaled or letterboxed.
// Where the picture sits inside that surface, in its own pixels
function FrameRect(src:TFrameSource=TFrameSource.presented):TRect;
// Map a canvas-space rect/point into those pixels
function CanvasToPixels(const r:TRect;src:TFrameSource=TFrameSource.presented):TRect; overload;
function CanvasToPixels(const p:TPoint;src:TFrameSource=TFrameSource.presented):TPoint; overload;
// Read a pixel region of the frame into image (its size must match the rect).
// Rows come out top-down regardless of the source.
procedure ReadFrameRect(const pixRect:TRect;image:TRawImage;src:TFrameSource=TFrameSource.presented);
end;
// Pointer of the current thread's window (the `window` threadvar), canvas space:
// read-only helpers for game code. Re-exported by Apus.Engine.API.
// A thread without a window is an error (asserted); in a release build its pointer is
// outside the canvas and every query is false.
Mouse=record
class function Pos:TVec2; static; // outside the canvas - ($3FFF,$3FFF)
class function X:single; static;
class function Y:single; static;
// Instant queries: the current state of the window, also inside the event handlers.
// Position queries are false while the pointer is outside the canvas
class function InRect(const r:TRect):boolean; overload; static; // right/bottom edges excluded
class function InRect(const r:TRect2):boolean; overload; static;
class function InCircle(x,y,radius:single):boolean; static;
// The pointer is not over the canvas. Pos is then the off-canvas sentinel, or the canvas
// edge while a button is held (a drag that left the canvas)
class function Outside:boolean; static;
// Any of the buttons (mbXXX mask) is held
class function Down(buttons:byte=mbLeft):boolean; static;
// Frame queries. A frame is one pass of the window's scene processing (TWindow.OnFrame):
// the input that came since the previous pass is published at its start. Use them in
// Process (frequency=0: one call per frame); Render may be skipped, a fixed-rate Process
// runs 0..N times per frame - take the input from the events there.
// Any of the buttons went down / up since the previous frame. A click shorter than a
// frame gives both, with Down false.
class function WasPressed(buttons:byte=mbLeft):boolean; static;
class function WasReleased(buttons:byte=mbLeft):boolean; static;
// The pointer was in the rect at the previous frame (false if it was outside the canvas)
class function WasInRect(const r:TRect):boolean; overload; static;
class function WasInRect(const r:TRect2):boolean; overload; static;
// Over a UI element that takes the input (as of the last hover dispatch)
class function OverUI:boolean; static;
// The virtual mouse is in control (see TVirtualInput)
class function Simulated:boolean; static;
end;
// Configuration hook: called on every rebuild, before the resolver.
// Set by TGameApplication (the engine core must not depend on it).
type
TSurfaceConfigHook=procedure(wnd:TWindow;const input:TSurfaceInput;var config:TSurfaceConfig) of object;
var
surfaceConfigHook:TSurfaceConfigHook;
function FindWindowByHandle(handle:THandle):TWindow;
function ListWindows:TWindowArray;
// Virtual mouse of an open window: '', '0' or 'main' - the main window, otherwise the
// window name (TWindow.name, case-insensitive). Any thread. The result is AddRef'ed -
// Release it; nil if there is no such window or it is closing.
function FindVirtualInput(const windowName:String8):TVirtualInput;
implementation
uses Types, SysUtils, Apus.EventMan, Apus.Lib, Apus.GfxFormats, Apus.Files, Apus.Strings,
{$IFDEF MSWINDOWS}Apus.Engine.Clipboard,{$ENDIF}
Apus.Engine.API, Apus.Engine.UIScene,
Apus.Engine.UITypes, Apus.Engine.TextDraw;
const
// TSurfacePending.mask bits
psmClient = 1; // client area size changed
psmDPI = 2; // system DPI changed
psmInsets = 4; // native safe-area insets changed
psmForce = 8; // rebuild requested explicitly (config/scale change)
OFF_CANVAS = $3FFF; // mousePos of a pointer outside the canvas
var
windowHash:TObjectHash;
// virtual mice of the open windows (see FindVirtualInput)
viRegistry:array of TVirtualInput;
viRegistryLock:TLock;
{ TScanSet }
procedure TScanSet.Clear;
begin
FillChar(bits,sizeof(bits),0);
end;
procedure TScanSet.Put(code:integer;value:boolean);
begin
if (code<0) or (code>=Scan.Count) then exit;
if value then bits[code shr 5]:=bits[code shr 5] or (cardinal(1) shl (code and 31))
else bits[code shr 5]:=bits[code shr 5] and not (cardinal(1) shl (code and 31));
end;
function TScanSet.Has(code:integer):boolean;
begin
result:=(code>=0) and (code<Scan.Count) and
(bits[code shr 5] and (cardinal(1) shl (code and 31))<>0);
end;
{ TVirtualInput }
constructor TVirtualInput.Create(owner:TWindow);
var
n:integer;
begin
lock.Init('VirtualInput',810);
refCount:=1; // the window's reference
ownerPtr:=owner;
ownerName:=owner.name;
applied.pos:=Types.Point(OFF_CANVAS,OFF_CANVAS);
viRegistryLock.Enter;
try
n:=length(viRegistry);
SetLength(viRegistry,n+1);
viRegistry[n]:=self;
finally
viRegistryLock.Leave;
end;
end;
procedure TVirtualInput.AddRef;
begin
Atomic.Inc(refCount);
end;
procedure TVirtualInput.Release;
begin
if Atomic.Dec(refCount)>0 then exit;
lock.Cleanup;
Free;
end;
function TVirtualInput.Queue(op:TVirtualInputOp):int64;
begin
lock.Enter;
try
if closedValue then exit(0);
inc(issuedTicket);
op.ticket:=issuedTicket;
if opCount>=length(ops) then SetLength(ops,opCount*2+8);
ops[opCount]:=op;
inc(opCount);
UpdateDone;
case op.kind of
vioMode:if op.enable<>reqActive then begin // a mode change ends the gesture
reqActive:=op.enable;
reqButtons:=0;
reqKeys.Clear;
end;
vioReset:begin
reqButtons:=0;
reqKeys.Clear;
end;
vioKey:reqKeys.Put(op.scanCode,op.pressed);
vioButton:
if op.pressed then reqButtons:=reqButtons or (1 shl (op.button-1))
else reqButtons:=reqButtons and not (1 shl (op.button-1));
end;
result:=issuedTicket;
finally
lock.Leave;
end;
end;
function TVirtualInput.TicketState(ticket:int64):TVirtualInputTicket;
var
i:integer;
begin
lock.Enter;
try
if ticket>doneTicket then exit(vitPending);
for i:=0 to high(dropped) do
if (ticket>=dropped[i].lo) and (ticket<=dropped[i].hi) then exit(vitDropped);
result:=vitDone;
finally
lock.Leave;
end;
end;
// Tickets complete in order: the one being applied (if any), then the queued ones
procedure TVirtualInput.UpdateDone;
begin
if takenTicket<>0 then doneTicket:=takenTicket-1
else if opCount>0 then doneTicket:=ops[0].ticket-1
else doneTicket:=issuedTicket;
end;
procedure TVirtualInput.DropQueued;
var
n:integer;
begin
if opCount=0 then exit;
n:=length(dropped);
SetLength(dropped,n+1);
dropped[n].lo:=ops[0].ticket;
dropped[n].hi:=ops[opCount-1].ticket;
opCount:=0;
UpdateDone;
reqActive:=activeValue;
reqButtons:=applied.buttons;
reqKeys:=applied.keys;
end;
procedure TVirtualInput.Cancel;
begin
lock.Enter;
try
DropQueued;
finally
lock.Leave;
end;
end;
function TVirtualInput.RequestedActive:boolean;
begin
lock.Enter;
try
result:=reqActive;
finally
lock.Leave;
end;
end;
function TVirtualInput.RequestedButtons:byte;
begin
lock.Enter;
try
result:=reqButtons;
finally
lock.Leave;
end;
end;
function TVirtualInput.RequestedKeys:TScanSet;
begin
lock.Enter;
try
result:=reqKeys;
finally
lock.Leave;
end;
end;
function TVirtualInput.QueuedCount:integer;
begin
lock.Enter;
try
result:=opCount+byte(takenTicket<>0);
finally
lock.Leave;
end;
end;
function TVirtualInput.LastTicket:int64;
begin
lock.Enter;
try
result:=issuedTicket;
finally
lock.Leave;
end;
end;
function TVirtualInput.State:TVirtualInputState;
begin
lock.Enter;
try
result:=applied;
finally
lock.Leave;
end;
end;
function TVirtualInput.IsActive:boolean;
begin
result:=activeValue;
end;
function TVirtualInput.IsClosed:boolean;
begin
lock.Enter;
try
result:=closedValue;
finally
lock.Leave;
end;
end;
function TVirtualInput.IsMainWindow:boolean;
begin
result:=(ownerPtr<>nil) and (ownerPtr=pointer(mainWindow));
end;
function TVirtualInput.TakeNext(out op:TVirtualInputOp;nowMs:int64):boolean;
begin
if opCount=0 then exit(false); // a racy read is fine: the next frame takes it
lock.Enter;
try
result:=(opCount>0) and (takenTicket=0) and (nowMs>=lastTakeMs+ops[0].delayMs);
if not result then exit;
lastTakeMs:=nowMs;
op:=ops[0];
dec(opCount);
if opCount>0 then Move(ops[1],ops[0],opCount*sizeof(ops[0]));
takenTicket:=op.ticket;
UpdateDone;
finally
lock.Leave;
end;
end;
procedure TVirtualInput.Complete(const ticket:int64;const st:TVirtualInputState);
begin
lock.Enter;
try
applied:=st;
if (ticket=0) or (ticket<>takenTicket) then exit; // state refresh only
takenTicket:=0;
UpdateDone;
finally
lock.Leave;
end;
end;
procedure TVirtualInput.SetActive(enable:boolean);
begin
activeValue:=enable;
end;
procedure TVirtualInput.Close;
var
i:integer;
begin
lock.Enter;
try
if closedValue then exit;
closedValue:=true;
DropQueued;
if takenTicket<>0 then begin // the window stops in the middle of an operation
i:=length(dropped);
SetLength(dropped,i+1);
dropped[i].lo:=takenTicket;
dropped[i].hi:=takenTicket;
takenTicket:=0;
end;
UpdateDone;
reqActive:=false;
reqButtons:=0;
reqKeys.Clear;
ownerPtr:=nil;
finally
lock.Leave;
end;
viRegistryLock.Enter;
try
for i:=high(viRegistry) downto 0 do
if viRegistry[i]=self then begin
viRegistry[i]:=viRegistry[high(viRegistry)];
SetLength(viRegistry,high(viRegistry));
end;
finally
viRegistryLock.Leave;
end;
end;
function FindVirtualInput(const windowName:String8):TVirtualInput;
var
i:integer;
main:boolean;
name:String8;
begin
result:=nil;
name:=windowName.ToLower;
main:=(name='') or (name='0') or (name='main');
viRegistryLock.Enter;
try
for i:=0 to high(viRegistry) do
if main and viRegistry[i].IsMainWindow or
not main and viRegistry[i].ownerName.Same(name) then begin
result:=viRegistry[i];
result.AddRef;
exit;
end;
finally
viRegistryLock.Leave;
end;
end;
function IsValidWindowName(const name:String8):boolean;
var
i:integer;
begin
result:=name<>'';
for i:=1 to length(name) do
if not (name[i] in ['A'..'Z','a'..'z','0'..'9','_','-','.']) then exit(false);
end;
constructor TWindow.Create(const windowName:String8);
begin
inherited Create;
ASSERT(IsValidWindowName(windowName),'Window name must be an ASCII identifier: '+windowName);
name:=windowName;
// levels (higher = inner): the window lock is the outermost engine runtime lock,
// pending surface data and the game object are entered inside it
runtimeLock.Init('Window',20);