psychmetal 0.5.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 PsychMetal contributors
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,6 @@
1
+ # The source distribution: what pip needs to build the extension, and nothing
2
+ # else. The prebuilt binaries in the repository are left out; pip builds its own.
3
+ include PsychMetalEngine.h PsychMetalInternal.h PsychMetalKeyboardQueue.h PsychMetalShaders.h
4
+ include python/README.md
5
+ global-exclude *.so *.mex *.mexmaca64 *.o
6
+ prune tests
@@ -0,0 +1,56 @@
1
+ Metadata-Version: 2.4
2
+ Name: psychmetal
3
+ Version: 0.5.0
4
+ Summary: Native Metal stimulus presentation on Apple silicon, with a Psychtoolbox Screen-like interface
5
+ Author-email: Keith Schneider <keith@alumni.caltech.edu>
6
+ License-Expression: MIT
7
+ Project-URL: Homepage, https://github.com/keiths0/PsychMetal
8
+ Project-URL: Documentation, https://github.com/keiths0/PsychMetal/blob/main/PsychMetal-0.5.0/README.md
9
+ Project-URL: Changelog, https://github.com/keiths0/PsychMetal/blob/main/PsychMetal-0.5.0/CHANGELOG.md
10
+ Keywords: psychophysics,vision science,stimulus presentation,Metal,Psychtoolbox
11
+ Classifier: Development Status :: 4 - Beta
12
+ Classifier: Intended Audience :: Science/Research
13
+ Classifier: Operating System :: MacOS
14
+ Classifier: Programming Language :: Python :: 3
15
+ Classifier: Programming Language :: C++
16
+ Classifier: Programming Language :: Objective C
17
+ Classifier: Topic :: Scientific/Engineering
18
+ Requires-Python: >=3.10
19
+ Description-Content-Type: text/markdown
20
+ License-File: LICENSE
21
+ Requires-Dist: numpy
22
+ Dynamic: license-file
23
+
24
+ # psychmetal
25
+
26
+ Native Metal stimulus presentation on Apple silicon Macs, from Python, with an interface resembling Psychtoolbox's Screen. There is no OpenGL anywhere in the path. The same engine serves [PsychMetal](https://github.com/keiths0/PsychMetal) for MATLAB and Octave.
27
+
28
+ Install it into a virtual environment. Homebrew's Python (and any Python following PEP 668) refuses packages installed outside one:
29
+
30
+ ```bash
31
+ python3 -m venv ~/venvs/psychmetal # once
32
+ source ~/venvs/psychmetal/bin/activate # in each new Terminal window
33
+ pip install psychmetal
34
+ ```
35
+
36
+ Requires macOS 14 or later on Apple silicon, an arm64 Python 3.10 or later, and numpy.
37
+
38
+ ```python
39
+ import psychmetal as pm
40
+
41
+ w, rect, ifi = pm.open_window(0, [0, 0, 0])
42
+ try:
43
+ pm.fill_rect(w, [255, 0, 0], [100, 100, 300, 300])
44
+ vbl, onset, flip_return, missed, slipped = pm.flip(w)
45
+ pm.wait_secs(1)
46
+ finally:
47
+ pm.close(w)
48
+ ```
49
+
50
+ Each PsychMetal command is a function with the snake_case name and the same arguments, order and defaults as in MATLAB; MATLAB's `[]` is `None`. Colours run 0–255 by default. Key indices are 0-based (`kb_name('ESCAPE')` is 40). Several rectangles, dot positions and per-item colours keep MATLAB's 4xN, 2xN and 3xN/4xN orientation, and images are numpy arrays (H, W[, C]) in any memory layout, read without copying. `help(psychmetal)` covers threading, batching and the other details.
51
+
52
+ Demos and hardware checks (Gabors, dynamic noise, textures, dot motion, keyboard, an inventory of every command) are in the repository's [python folder](https://github.com/keiths0/PsychMetal/tree/main/PsychMetal-0.5.0/python).
53
+
54
+ Flip timestamps come from Metal's presentation reports, not a photodiode. See the repository for validation and its limits.
55
+
56
+ MIT license.
@@ -0,0 +1,444 @@
1
+ // PsychMetalEngine.h — host-neutral engine for PsychMetal 0.5.0.
2
+ // SPDX-License-Identifier: MIT
3
+ //
4
+ // Plain C++17. It never includes mex.h, Python.h or an Objective-C header, so
5
+ // every front end can include it:
6
+ //
7
+ // PsychMetalMex.cpp MATLAB / Octave front end -> PsychMetalCore.mexmaca64 / .mex
8
+ // PsychMetalPython.cpp Python front end (C API) -> psychmetal/_psychmetal*.so
9
+ // PsychMetalEngine.mm the implementation: Metal, AppKit, CoreGraphics
10
+ //
11
+ // Boundary rules
12
+ // 1. Validation is shared, not duplicated. A front end checks only that a host
13
+ // value has the right kind (a real numeric scalar, a dense array) and then
14
+ // applies the shared scalar conventions below (checkFinite, checkUnsigned),
15
+ // so both hosts word them identically. Everything else (ranges, state,
16
+ // capacity, per-element checks) happens inside the engine functions.
17
+ // 2. Failures throw pm::Error{id, message}. The engine never calls a host
18
+ // error function and never longjmps. Front ends catch at one boundary.
19
+ // 3. Arrays cross as ArrayView: pointer, element type, logical shape, byte
20
+ // strides. The engine never assumes column- or row-major order, so MATLAB
21
+ // and numpy arrays are both read in place.
22
+ // 4. PM_BLOCKING marks calls that can wait on the display, the GPU, a condition
23
+ // variable, a thread join or the main thread. A front end with a global
24
+ // interpreter lock must not hold it across those calls; the Python front
25
+ // end releases it around every engine call, which is simpler and cheap.
26
+ // 5. One caller thread at a time. The engine synchronises against its own
27
+ // Metal and keyboard threads, not against concurrent host calls.
28
+ // 6. No engine callback re-enters the host. Warnings go through HostHooks::warn,
29
+ // which is called on the caller thread, inside the engine call.
30
+
31
+ #ifndef PSYCHMETAL_ENGINE_H
32
+ #define PSYCHMETAL_ENGINE_H
33
+
34
+ #include <array>
35
+ #include <cstddef>
36
+ #include <cstdint>
37
+ #include <optional>
38
+ #include <stdexcept>
39
+ #include <string>
40
+ #include <vector>
41
+
42
+ // Marker only; expands to nothing.
43
+ #define PM_BLOCKING
44
+
45
+ namespace pm {
46
+
47
+ // ---------------------------------------------------------------------------
48
+ // Version and errors
49
+ // ---------------------------------------------------------------------------
50
+
51
+ // Engine version. Both front ends return it for 'Version'; PsychMetal.m and the
52
+ // psychmetal package refuse to run against a different value.
53
+ inline constexpr const char *kEngineVersion = "0.5.0";
54
+
55
+ // Every error and warning identifier the engine emits.
56
+ inline constexpr const char *kErrGeneral = "PsychMetal:Error";
57
+ inline constexpr const char *kErrMetal = "PsychMetal:Metal";
58
+ inline constexpr const char *kErrScreen = "PsychMetal:Screen";
59
+ inline constexpr const char *kErrMode = "PsychMetal:Mode";
60
+ inline constexpr const char *kErrDrawableSize = "PsychMetal:DrawableSize";
61
+ inline constexpr const char *kErrPipeline = "PsychMetal:Pipeline";
62
+ inline constexpr const char *kErrShader = "PsychMetal:Shader";
63
+ inline constexpr const char *kErrWindowOrigin = "PsychMetal:WindowOrigin";
64
+ inline constexpr const char *kErrNative = "PsychMetal:NativeException"; // front ends: std::exception
65
+ inline constexpr const char *kWarnRefresh = "PsychMetal:UnknownRefreshRate";
66
+ inline constexpr const char *kWarnDisplayMapping = "PsychMetal:DisplayMapping";
67
+ inline constexpr const char *kWarnDisplayCapture = "PsychMetal:DisplayCapture";
68
+ inline constexpr const char *kWarnDrainTimeout = "PsychMetal:DrainTimeout";
69
+
70
+ class Error : public std::runtime_error {
71
+ public:
72
+ Error(std::string id, const std::string &message)
73
+ : std::runtime_error(message), id_(std::move(id)) {}
74
+ explicit Error(const std::string &message) : Error(kErrGeneral, message) {}
75
+ const std::string &id() const noexcept { return id_; }
76
+ private:
77
+ std::string id_;
78
+ };
79
+
80
+ // ---------------------------------------------------------------------------
81
+ // Shared scalar conventions for the front ends
82
+ // ---------------------------------------------------------------------------
83
+
84
+ // "<name> must be a real numeric scalar." Front ends call this when a host value
85
+ // is not a real, dense, one-element number.
86
+ [[noreturn]] void failNotScalar(const char *name);
87
+ // Returns v; throws "<name> must be finite." otherwise.
88
+ double checkFinite(double v, const char *name);
89
+ // checkFinite, then "<name> must be a nonnegative integer in range." unless v is
90
+ // an integer in [0, maximum].
91
+ uint64_t checkUnsigned(double v, const char *name, uint64_t maximum);
92
+ // Largest integer a double represents exactly; the ceiling for handles and tokens.
93
+ inline constexpr uint64_t kMaxId = 9007199254740991ULL;
94
+
95
+ // ---------------------------------------------------------------------------
96
+ // Host hooks and lifecycle
97
+ // ---------------------------------------------------------------------------
98
+
99
+ struct HostHooks {
100
+ // Replaces mexWarnMsgIdAndTxt. Called on the caller thread during an engine call.
101
+ void (*warn)(const char *id, const char *message) = nullptr;
102
+ // Replace mexLock / mexUnlock: called when the engine starts or stops holding
103
+ // state that asynchronous callbacks reference (a window, a keyboard queue).
104
+ void (*pinModule)() = nullptr;
105
+ void (*unpinModule)() = nullptr;
106
+ };
107
+ void installHostHooks(const HostHooks &hooks);
108
+
109
+ // Release every native resource. Idempotent and noexcept. The MEX front end
110
+ // registers it with mexAtExit; the Python front end with atexit.
111
+ PM_BLOCKING void shutdown() noexcept;
112
+
113
+ // ---------------------------------------------------------------------------
114
+ // Array views
115
+ // ---------------------------------------------------------------------------
116
+
117
+ // Other: any element type the engine does not accept (complex, sparse, int16,
118
+ // float16, ...). Passing it lets the engine report its own message.
119
+ enum class ScalarType : uint8_t { Float64, Float32, UInt8, Bool, Other };
120
+
121
+ // A read-only strided view. Strides are in bytes. Logical shapes:
122
+ // image (H, W) or (H, W, C), C in {1, 3, 4}
123
+ // per-shape (N) for kind and param; (N, 4) for rect, color and extra
124
+ // A MATLAB 4xN matrix and a numpy C-contiguous (N, 4) array occupy the same
125
+ // memory: both are (N, 4) with strides (4*8, 8).
126
+ struct ArrayView {
127
+ const void *data = nullptr;
128
+ ScalarType type = ScalarType::Other;
129
+ int ndim = 0;
130
+ std::array<size_t, 3> shape{};
131
+ std::array<ptrdiff_t, 3> strides{};
132
+ size_t count() const noexcept {
133
+ if (ndim <= 0) return 0;
134
+ size_t n = 1;
135
+ for (int i = 0; i < ndim; i++) n *= shape[size_t(i)];
136
+ return n;
137
+ }
138
+ };
139
+
140
+ // A writable strided view of doubles, allocated by the front end.
141
+ struct MutableArrayView {
142
+ double *data = nullptr;
143
+ int ndim = 0;
144
+ std::array<size_t, 3> shape{};
145
+ std::array<ptrdiff_t, 3> strides{};
146
+ };
147
+
148
+ using Rect4 = std::array<double, 4>;
149
+
150
+ // ---------------------------------------------------------------------------
151
+ // Main thread (no MEX command)
152
+ // ---------------------------------------------------------------------------
153
+
154
+ // True on the process main thread (the AppKit thread).
155
+ bool onMainThread() noexcept;
156
+ // Service AppKit on the main thread for up to `seconds`, returning early once
157
+ // queued events are drained. Key events are discarded: the engine polls the
158
+ // keyboard and an unhandled keyDown would beep. Throws off the main thread.
159
+ // Python's run(threaded=True) parks the main thread in a loop of these calls
160
+ // while the experiment runs on a worker, which is how MATLAB hosts the engine.
161
+ PM_BLOCKING void serviceMainRunLoop(double seconds);
162
+
163
+ // ---------------------------------------------------------------------------
164
+ // Session lifecycle
165
+ // ---------------------------------------------------------------------------
166
+
167
+ // 'Version'
168
+ const char *version() noexcept;
169
+
170
+ // 'PrepareApp'. Closes any open session, then activates the host as a regular app.
171
+ PM_BLOCKING void prepareApp();
172
+
173
+ // 'TimingPolicy'. 0 baseline, 1 bounded submissions, 2 bounded + settled startup.
174
+ int timingPolicy() noexcept;
175
+ void setTimingPolicy(uint64_t policy); // before Open
176
+
177
+ struct OpenOptions {
178
+ double screenIndex = 0; // CoreGraphics active-display index; -1 = last
179
+ uint64_t drawableCount = 3; // 2 or 3
180
+ bool waitForConfirm = true;
181
+ bool displaySync = true;
182
+ bool captureDisplay = true;
183
+ std::optional<double> refreshHz; // 20..1000; overrides the reported rate
184
+ };
185
+ struct OpenResult {
186
+ double pixelWidth, pixelHeight; // render size in pixels
187
+ double ifi; // nominal refresh period, seconds
188
+ double pointWidth, pointHeight; // display size in points
189
+ uint64_t sessionToken;
190
+ };
191
+ // 'Open'. Requires prepareApp().
192
+ PM_BLOCKING OpenResult openSession(const OpenOptions &options);
193
+
194
+ // One startup presentation attempt: a row of the StartupHistory matrix.
195
+ struct StartupRecord {
196
+ uint64_t token;
197
+ int status; // 0 confirmed, 1 missing, 2 pending, 3 GPU error, 4 no drawable
198
+ double presentedTime;
199
+ double callbackTime;
200
+ int gpuDone;
201
+ double committedTime;
202
+ };
203
+ // 'StartupHistory'
204
+ std::vector<StartupRecord> startupHistory();
205
+ // 'ConfirmStartup'. Returns the startup history; on failure throws, and
206
+ // startupHistory() still returns the attempts.
207
+ PM_BLOCKING std::vector<StartupRecord> confirmStartup();
208
+
209
+ // 'SettleWindow'. 0..1 s. Requires an open window.
210
+ PM_BLOCKING void settleWindow(double seconds);
211
+
212
+ // 'Close'. Idempotent.
213
+ PM_BLOCKING void closeSession();
214
+
215
+ // ---------------------------------------------------------------------------
216
+ // Presentation
217
+ // ---------------------------------------------------------------------------
218
+
219
+ struct FlipResult {
220
+ double time; // confirmed presentation time, else projected
221
+ bool confirmed;
222
+ double slipRefreshes; // slip of the previous confirmed frame, whole refreshes
223
+ double gridPeriod; // measured period if available, else nominal
224
+ double queueMs; // time spent in enqueue
225
+ double callMs; // whole call
226
+ double returnTime;
227
+ uint64_t token;
228
+ };
229
+ // 'Flip'. when == 0 means as soon as possible; when < 0 throws.
230
+ PM_BLOCKING FlipResult flip(double when = 0);
231
+
232
+ // 'Queue'. A supplied `when` counts as a target even when it is 0.
233
+ PM_BLOCKING uint64_t queueFrame(std::optional<double> when);
234
+
235
+ struct ScheduleResult {
236
+ double time; // confirmed presentation time, else projected
237
+ bool scheduled; // the MEX column 2 reports !scheduled as 2
238
+ bool confirmed;
239
+ double slipRefreshes;
240
+ double gridPeriod;
241
+ };
242
+ // 'WaitScheduled'. Times out two seconds after max(now, when).
243
+ PM_BLOCKING ScheduleResult waitScheduled(uint64_t token, std::optional<double> when);
244
+
245
+ // 'PrepareFlip' / 'PresentNow'
246
+ PM_BLOCKING uint64_t prepareFlip();
247
+ struct PresentResult { double time; double callMs; };
248
+ PresentResult presentNow();
249
+
250
+ // 'SetDisplaySync'
251
+ PM_BLOCKING void setDisplaySync(bool enabled);
252
+
253
+ // 'PrefetchDrawable'
254
+ void setPrefetchDrawable(bool enabled);
255
+
256
+ // ---------------------------------------------------------------------------
257
+ // Refresh grid
258
+ // ---------------------------------------------------------------------------
259
+
260
+ struct GridAnchor { double anchor; double period; double samples; };
261
+ // 'GridAnchor'. anchor is NaN before the first confirmed frame.
262
+ GridAnchor gridAnchor();
263
+ // 'NextPhase'. NaN before the first confirmed frame.
264
+ double nextPhase(double after, double phase);
265
+ // 'NextRefresh'
266
+ double nextRefresh(double after);
267
+
268
+ struct WaitToDrawResult { double wokeAt; double lead; double deadline; };
269
+ // 'WaitToDraw'. budget >= 0 seconds.
270
+ PM_BLOCKING WaitToDrawResult waitToDraw(double target, double budget);
271
+
272
+ // ---------------------------------------------------------------------------
273
+ // Drawing
274
+ // ---------------------------------------------------------------------------
275
+
276
+ // 'SetBackgroundColor'. Components 0..1.
277
+ void setBackgroundColor(double r, double g, double b, double a);
278
+
279
+ // 'AddShapes'. kind and param (N); rect, color and extra (N, 4); all Float64.
280
+ void addShapes(const ArrayView &kind, const ArrayView &param,
281
+ const ArrayView &rect, const ArrayView &color, const ArrayView &extra);
282
+
283
+ // 'MakeTexture' / 'UpdateTexture'. Image (H, W[, C]) of Float64, Float32, UInt8
284
+ // or Bool; H, W in 1..16384; C in {1, 3, 4}. UInt8 scales by 1/255.
285
+ uint64_t makeTexture(const ArrayView &image);
286
+ void updateTexture(uint64_t handle, const ArrayView &image);
287
+
288
+ // 'DrawTextures'. handle, angle and filterMode (N); src, dst and tint (N, 4); all
289
+ // Float64. src is normalised, dst in pixels, angle in radians about the
290
+ // destination centre, tint 0..1, filterMode 0 nearest or 1 bilinear. Drawn in
291
+ // order. Every entry is checked before any is queued, so a rejected call leaves
292
+ // the frame as it was.
293
+ void drawTextures(const ArrayView &handle, const ArrayView &src, const ArrayView &dst,
294
+ const ArrayView &angle, const ArrayView &tint, const ArrayView &filterMode);
295
+
296
+ // 'CloseTexture'
297
+ void closeTexture(uint64_t handle);
298
+
299
+ // 'NoiseValues'. checkNoiseRequest validates so a front end never allocates for a
300
+ // bad request; noiseValues then fills (H, W) or (H, W, 3) in the front end's
301
+ // layout with exactly the values the GPU noise shape draws.
302
+ struct NoiseRequest {
303
+ double width, height; // integers 1..16384
304
+ double seed; // integer 0..16777215
305
+ bool normal;
306
+ bool colour;
307
+ std::array<double, 3> mean;
308
+ double spread; // >= 0
309
+ };
310
+ void checkNoiseRequest(const NoiseRequest &request);
311
+ void noiseValues(const NoiseRequest &request, const MutableArrayView &out);
312
+
313
+ // ---------------------------------------------------------------------------
314
+ // Display modes and cursor
315
+ // ---------------------------------------------------------------------------
316
+
317
+ struct DisplayMode {
318
+ double pointWidth, pointHeight, pixelWidth, pixelHeight, refreshHz;
319
+ };
320
+ // 'Modes'. The current mode first. screenIndex -1 = last display.
321
+ std::vector<DisplayMode> modes(double screenIndex);
322
+ // 'SetMode'. Requires a closed window; picks the matching point size with the
323
+ // most pixels. (The MEX front end still returns 1.)
324
+ void setMode(double screenIndex, double pointWidth, double pointHeight);
325
+ // 'Cursor'
326
+ void setCursorVisible(bool visible);
327
+
328
+ // ---------------------------------------------------------------------------
329
+ // Input
330
+ // ---------------------------------------------------------------------------
331
+
332
+ // Index k of every 256-element key array is HID usage k+1 (MATLAB keyCode(k+1)).
333
+ // KeyEvent::key is the HID usage (1-based).
334
+
335
+ struct MouseState { double x, y; std::array<bool, 3> buttons; }; // left, right, centre
336
+ // 'Mouse'. Pixel coordinates on the presentation display.
337
+ MouseState mouse();
338
+
339
+ struct KeyState {
340
+ bool anyDown;
341
+ double secs;
342
+ std::array<bool, 256> down;
343
+ double securePid; // 0 inactive, -1 active (owner not queried)
344
+ };
345
+ // 'Keys'
346
+ KeyState keys();
347
+
348
+ struct KbQueueStatus {
349
+ bool created, running;
350
+ double pollInterval, lastScanInterval, maxScanInterval;
351
+ uint64_t scans, dropped;
352
+ double secureInputPID;
353
+ };
354
+ // 'KbQueueStatus'
355
+ KbQueueStatus kbQueueStatus();
356
+ // 'KbQueueCreate'. mask nonzero = watched, all finite; interval 0.001..0.1 s.
357
+ void kbQueueCreate(const std::array<double, 256> &mask, double interval);
358
+ // 'KbQueueRelease' / 'KbQueueStart' / 'KbQueueStop' / 'KbQueueFlush'
359
+ PM_BLOCKING void kbQueueRelease();
360
+ void kbQueueStart();
361
+ PM_BLOCKING void kbQueueStop();
362
+ void kbQueueFlush();
363
+
364
+ struct KeyEvent { double time; int key; bool pressed; };
365
+ struct KbEvents { std::vector<KeyEvent> events; uint64_t dropped; };
366
+ // 'KbQueueGetEvents'
367
+ KbEvents kbQueueGetEvents();
368
+
369
+ struct KbCheck {
370
+ bool pressed;
371
+ std::array<double, 256> firstPress, firstRelease, lastPress, lastRelease;
372
+ };
373
+ // 'KbQueueCheck'
374
+ KbCheck kbQueueCheck();
375
+
376
+ // ---------------------------------------------------------------------------
377
+ // Time
378
+ // ---------------------------------------------------------------------------
379
+
380
+ // 'Now'. CACurrentMediaTime, the clock of every timestamp here.
381
+ double now() noexcept;
382
+ // 'Wait'. Sleep, then spin, until the absolute deadline; returns the time on return.
383
+ PM_BLOCKING double waitUntil(double deadline);
384
+
385
+ // ---------------------------------------------------------------------------
386
+ // Diagnostics
387
+ // ---------------------------------------------------------------------------
388
+
389
+ // One presented frame: a row of the Diagnostic history matrix, in column order.
390
+ struct FrameRecord {
391
+ uint64_t token;
392
+ double projected;
393
+ double presented;
394
+ int status; // done ? status : 2
395
+ double scheduledAt;
396
+ double callback;
397
+ int commandStatus;
398
+ double requestedTime;
399
+ double gpuStart, gpuEnd;
400
+ double presentRequest;
401
+ double presentCallMs;
402
+ double committedAt;
403
+ double drawableAcquireMs, encodeMs, prefetchMs, pacingMs;
404
+ };
405
+
406
+ // The Diagnostic summary; field names and order are those of the MATLAB struct.
407
+ struct DiagnosticSummary {
408
+ double confirmedPresentations, missingPresentedTimes;
409
+ double lastTargetErrorMs, lastConfirmDelayMs, appKitScreenIndex;
410
+ double cgDisplayID, renderWidth, renderHeight;
411
+ double drawableWidth, drawableHeight, inFlight;
412
+ double requestedDrawableCount, drawableCountReadback;
413
+ std::string hostBundleIdentifier;
414
+ double activationPolicyBefore, activationPolicyAfter;
415
+ bool activationPolicyPromotionAttempted, activationPolicyPromotionSucceeded;
416
+ std::string macOSVersion, processName;
417
+ double machTimebaseHz, machTickNanoseconds;
418
+ bool waitForConfirm;
419
+ double measuredRefreshHz, gridSamples, directNoDrawable, directConfirmTimeouts;
420
+ double leadEstimateMs, pipelineEstimateMs, gpuEstimateMs;
421
+ bool displaySyncEnabled, displayCaptured;
422
+ double modePointWidth, modePixelWidth, largestModePixelWidth;
423
+ double shapesAppended, shapesEncoded, shapeEncodeCalls;
424
+ double texturesCreated, texturesDrawn, textureAllocations, textureUpdates;
425
+ double lastTextureUploadMs;
426
+ Rect4 lastShapeRect, lastShapeColor;
427
+ double lastShapeKind;
428
+ Rect4 windowFrame, viewBounds, layerFrame;
429
+ Rect4 screenFrame, screenVisibleFrame, screenSafeAreaInsets; // insets: top, left, bottom, right
430
+ Rect4 cgDisplayBounds;
431
+ double backingScaleFactor, timingPolicy;
432
+ double keyScanMaxMs, secureQueryMaxMs, keyScanMeanMs, secureQueryMeanMs, keyReadCount;
433
+ };
434
+
435
+ struct DiagnosticReport {
436
+ std::vector<FrameRecord> history; // user frames since ConfirmStartup, newest 16384
437
+ DiagnosticSummary summary;
438
+ };
439
+ // 'Diagnostic'. Waits up to two seconds for in-flight frames first.
440
+ PM_BLOCKING DiagnosticReport diagnostic();
441
+
442
+ } // namespace pm
443
+
444
+ #endif // PSYCHMETAL_ENGINE_H