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.
- psychmetal-0.5.0/LICENSE +21 -0
- psychmetal-0.5.0/MANIFEST.in +6 -0
- psychmetal-0.5.0/PKG-INFO +56 -0
- psychmetal-0.5.0/PsychMetalEngine.h +444 -0
- psychmetal-0.5.0/PsychMetalEngine.mm +1989 -0
- psychmetal-0.5.0/PsychMetalInternal.h +29 -0
- psychmetal-0.5.0/PsychMetalKeyboardQueue.h +90 -0
- psychmetal-0.5.0/PsychMetalPython.cpp +822 -0
- psychmetal-0.5.0/PsychMetalShaders.h +98 -0
- psychmetal-0.5.0/PsychMetalShared.cpp +165 -0
- psychmetal-0.5.0/README.md +128 -0
- psychmetal-0.5.0/pyproject.toml +44 -0
- psychmetal-0.5.0/python/README.md +33 -0
- psychmetal-0.5.0/python/psychmetal/__init__.py +999 -0
- psychmetal-0.5.0/python/psychmetal.egg-info/PKG-INFO +56 -0
- psychmetal-0.5.0/python/psychmetal.egg-info/SOURCES.txt +19 -0
- psychmetal-0.5.0/python/psychmetal.egg-info/dependency_links.txt +1 -0
- psychmetal-0.5.0/python/psychmetal.egg-info/requires.txt +1 -0
- psychmetal-0.5.0/python/psychmetal.egg-info/top_level.txt +1 -0
- psychmetal-0.5.0/setup.cfg +4 -0
- psychmetal-0.5.0/setup.py +46 -0
psychmetal-0.5.0/LICENSE
ADDED
|
@@ -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 ¶m,
|
|
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
|