scrollkit 0.8.3__py3-none-any.whl
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.
- scrollkit/__init__.py +16 -0
- scrollkit/app/__init__.py +6 -0
- scrollkit/app/base.py +918 -0
- scrollkit/app/memory.py +70 -0
- scrollkit/config/__init__.py +1 -0
- scrollkit/config/settings_manager.py +215 -0
- scrollkit/config/transition_names.py +31 -0
- scrollkit/dev/__init__.py +41 -0
- scrollkit/dev/capabilities.py +374 -0
- scrollkit/dev/harness.py +383 -0
- scrollkit/dev/metrics.py +91 -0
- scrollkit/dev/performance.py +174 -0
- scrollkit/dev/validation.py +245 -0
- scrollkit/display/__init__.py +14 -0
- scrollkit/display/_graphics.py +299 -0
- scrollkit/display/_recording.py +165 -0
- scrollkit/display/_sim_backend.py +99 -0
- scrollkit/display/bitmap_text.py +408 -0
- scrollkit/display/boards.py +186 -0
- scrollkit/display/colors.py +176 -0
- scrollkit/display/content.py +604 -0
- scrollkit/display/gradient_text.py +167 -0
- scrollkit/display/interface.py +126 -0
- scrollkit/display/simulator.py +80 -0
- scrollkit/display/text_fill.py +59 -0
- scrollkit/display/text_pixels.py +250 -0
- scrollkit/display/unified.py +595 -0
- scrollkit/effects/__init__.py +28 -0
- scrollkit/effects/drip_splash.py +253 -0
- scrollkit/effects/easing.py +131 -0
- scrollkit/effects/overlay.py +92 -0
- scrollkit/effects/particles.py +355 -0
- scrollkit/effects/reveal_splash.py +132 -0
- scrollkit/effects/scrolling.py +363 -0
- scrollkit/effects/swarm_reveal.py +512 -0
- scrollkit/effects/text_render.py +25 -0
- scrollkit/effects/transitions.py +873 -0
- scrollkit/exceptions.py +55 -0
- scrollkit/network/__init__.py +1 -0
- scrollkit/network/http_client.py +505 -0
- scrollkit/network/mdns.py +44 -0
- scrollkit/network/wifi_manager.py +382 -0
- scrollkit/ota/__init__.py +13 -0
- scrollkit/ota/client.py +528 -0
- scrollkit/ota/display_progress.py +125 -0
- scrollkit/ota/manifest.py +206 -0
- scrollkit/ota/publish.py +379 -0
- scrollkit/simulator/ATTRIBUTION.md +20 -0
- scrollkit/simulator/CIRCUITPYTHON_COMPATIBILITY.md +364 -0
- scrollkit/simulator/LICENSE +176 -0
- scrollkit/simulator/README.md +87 -0
- scrollkit/simulator/__init__.py +11 -0
- scrollkit/simulator/adafruit_bitmap_font/__init__.py +5 -0
- scrollkit/simulator/adafruit_bitmap_font/bitmap_font.py +273 -0
- scrollkit/simulator/adafruit_bitmap_font/glyph_cache.py +70 -0
- scrollkit/simulator/adafruit_display_text/__init__.py +5 -0
- scrollkit/simulator/adafruit_display_text/label.py +336 -0
- scrollkit/simulator/bitmaptools.py +50 -0
- scrollkit/simulator/core/__init__.py +8 -0
- scrollkit/simulator/core/color_utils.py +62 -0
- scrollkit/simulator/core/device_benchmarks.json +254 -0
- scrollkit/simulator/core/feasibility.py +160 -0
- scrollkit/simulator/core/hardware_profile.py +191 -0
- scrollkit/simulator/core/led_matrix.py +307 -0
- scrollkit/simulator/core/matrixportal_s3_baseline.json +12 -0
- scrollkit/simulator/core/performance_manager.py +253 -0
- scrollkit/simulator/core/pixel_buffer.py +188 -0
- scrollkit/simulator/devices/__init__.py +7 -0
- scrollkit/simulator/devices/base_device.py +79 -0
- scrollkit/simulator/devices/matrixportal_s3.py +87 -0
- scrollkit/simulator/displayio/__init__.py +12 -0
- scrollkit/simulator/displayio/bitmap.py +158 -0
- scrollkit/simulator/displayio/display.py +195 -0
- scrollkit/simulator/displayio/fourwire.py +54 -0
- scrollkit/simulator/displayio/group.py +125 -0
- scrollkit/simulator/displayio/ondiskbitmap.py +109 -0
- scrollkit/simulator/displayio/palette.py +115 -0
- scrollkit/simulator/displayio/tilegrid.py +155 -0
- scrollkit/simulator/fonts/3x5.bdf +2474 -0
- scrollkit/simulator/fonts/Arial_16.bdf +7366 -0
- scrollkit/simulator/fonts/Arial_16.bdf.license +3 -0
- scrollkit/simulator/fonts/Arial_Bold_12.bdf +6131 -0
- scrollkit/simulator/fonts/Arial_Bold_12.bdf.license +2 -0
- scrollkit/simulator/fonts/Arial_Bold_18.bdf +32653 -0
- scrollkit/simulator/fonts/Arial_Bold_18.bdf.license +2 -0
- scrollkit/simulator/fonts/Junction_regular_24.bdf +8676 -0
- scrollkit/simulator/fonts/Junction_regular_24.bdf.license +3 -0
- scrollkit/simulator/fonts/LeagueSpartan-Bold-16.bdf +12458 -0
- scrollkit/simulator/fonts/LeagueSpartan-Bold-16.bdf.license +1921 -0
- scrollkit/simulator/fonts/LeagueSpartan_Bold_16.bdf +12458 -0
- scrollkit/simulator/fonts/LeagueSpartan_Bold_16.bdf.license +4 -0
- scrollkit/simulator/fonts/LibreBodoniv2002-Bold-27.bdf +16818 -0
- scrollkit/simulator/fonts/LibreBodoniv2002-Bold-27.bdf.license +1921 -0
- scrollkit/simulator/fonts/tom-thumb.bdf +2353 -0
- scrollkit/simulator/fonts/viii-bold.bdf +2673 -0
- scrollkit/simulator/fonts/viii.bdf +2659 -0
- scrollkit/simulator/terminalio/__init__.py +20 -0
- scrollkit/utils/__init__.py +1 -0
- scrollkit/utils/color_utils.py +54 -0
- scrollkit/utils/diagnostics.py +227 -0
- scrollkit/utils/error_handler.py +347 -0
- scrollkit/utils/system_utils.py +245 -0
- scrollkit/utils/url_utils.py +46 -0
- scrollkit/web/__init__.py +6 -0
- scrollkit/web/settings_server.py +328 -0
- scrollkit/web/wifi_setup.py +331 -0
- scrollkit-0.8.3.dist-info/METADATA +248 -0
- scrollkit-0.8.3.dist-info/RECORD +111 -0
- scrollkit-0.8.3.dist-info/WHEEL +5 -0
- scrollkit-0.8.3.dist-info/licenses/LICENSE +31 -0
- scrollkit-0.8.3.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
# Copyright (c) 2024-2026 Michael Winslow Czeiszperger
|
|
2
|
+
"""Desktop emulation of CircuitPython's ``bitmaptools`` C module.
|
|
3
|
+
|
|
4
|
+
ScrollKit's span/rect painters, overlay-mask, and bitmap-text all express bounded
|
|
5
|
+
work through ``bitmaptools.fill_region`` / ``bitmaptools.blit`` so the SAME code
|
|
6
|
+
runs on the device (where ``bitmaptools`` is a built-in) and on the desktop
|
|
7
|
+
simulator (where this shim provides it). The display resolves which module to use
|
|
8
|
+
via ``display.gfx``.
|
|
9
|
+
|
|
10
|
+
Semantics mirror the device for the subset ScrollKit uses:
|
|
11
|
+
- ``fill_region`` fills the HALF-OPEN rectangle ``[x1, x2) x [y1, y2)`` clipped to
|
|
12
|
+
the bitmap; an empty/inverted region is a no-op.
|
|
13
|
+
- ``blit`` copies a source sub-rectangle into the destination at ``(x, y)`` with
|
|
14
|
+
``skip_index`` transparency, clipped at all four edges (incl. negative offsets).
|
|
15
|
+
|
|
16
|
+
Desktop-only — never imported on CircuitPython (the device uses the real module).
|
|
17
|
+
A golden corpus captured on a real board pins this shim's fidelity
|
|
18
|
+
(``test/unit/display/test_bitmaptools_shim.py``); if it ever diverges, fix the
|
|
19
|
+
shim, never the shared effect logic.
|
|
20
|
+
"""
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def fill_region(bitmap, x1, y1, x2, y2, value):
|
|
24
|
+
"""Fill the half-open rectangle ``[x1, x2) x [y1, y2)`` with ``value``.
|
|
25
|
+
|
|
26
|
+
Clipped to the bitmap. Matching CircuitPython, an inverted or empty region
|
|
27
|
+
(``x1 >= x2`` or ``y1 >= y2``) is a no-op — the bounds are NOT reordered, so
|
|
28
|
+
the simulator and device agree.
|
|
29
|
+
"""
|
|
30
|
+
if x1 >= x2 or y1 >= y2:
|
|
31
|
+
return
|
|
32
|
+
w = bitmap.width
|
|
33
|
+
h = bitmap.height
|
|
34
|
+
xa = x1 if x1 > 0 else 0
|
|
35
|
+
ya = y1 if y1 > 0 else 0
|
|
36
|
+
xb = x2 if x2 < w else w
|
|
37
|
+
yb = y2 if y2 < h else h
|
|
38
|
+
if xb <= xa or yb <= ya:
|
|
39
|
+
return
|
|
40
|
+
bitmap._buffer[ya:yb, xa:xb] = value
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def blit(dest, source, x, y, *, x1=0, y1=0, x2=None, y2=None, skip_index=None):
|
|
44
|
+
"""Copy ``source`` (sub-rect ``[x1,x2) x [y1,y2)``) into ``dest`` at ``(x, y)``.
|
|
45
|
+
|
|
46
|
+
Honors ``skip_index`` transparency and clips at every edge, including negative
|
|
47
|
+
destination offsets. Delegates to the simulator ``Bitmap.blit``, which already
|
|
48
|
+
implements the CircuitPython clipping/transparency rules.
|
|
49
|
+
"""
|
|
50
|
+
dest.blit(x, y, source, x1=x1, y1=y1, x2=x2, y2=y2, skip_index=skip_index)
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
# Copyright (c) 2024-2026 Michael Winslow Czeiszperger
|
|
2
|
+
"""Color manipulation utilities for LED simulation."""
|
|
3
|
+
|
|
4
|
+
def rgb565_to_rgb888(color565):
|
|
5
|
+
"""Convert RGB565 color to RGB888 (standard 24-bit RGB).
|
|
6
|
+
|
|
7
|
+
Args:
|
|
8
|
+
color565: 16-bit color value in RGB565 format
|
|
9
|
+
|
|
10
|
+
Returns:
|
|
11
|
+
Tuple of (r, g, b) values in 0-255 range
|
|
12
|
+
"""
|
|
13
|
+
r = ((color565 >> 11) & 0x1F) << 3
|
|
14
|
+
g = ((color565 >> 5) & 0x3F) << 2
|
|
15
|
+
b = (color565 & 0x1F) << 3
|
|
16
|
+
|
|
17
|
+
# Add the MSB bits to the LSB for better color accuracy
|
|
18
|
+
r |= r >> 5
|
|
19
|
+
g |= g >> 6
|
|
20
|
+
b |= b >> 5
|
|
21
|
+
|
|
22
|
+
return (r, g, b)
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def rgb888_to_rgb565(r, g, b):
|
|
26
|
+
"""Convert RGB888 color to RGB565.
|
|
27
|
+
|
|
28
|
+
Args:
|
|
29
|
+
r: Red component (0-255)
|
|
30
|
+
g: Green component (0-255)
|
|
31
|
+
b: Blue component (0-255)
|
|
32
|
+
|
|
33
|
+
Returns:
|
|
34
|
+
16-bit color value in RGB565 format
|
|
35
|
+
"""
|
|
36
|
+
return ((r & 0xF8) << 8) | ((g & 0xFC) << 3) | (b >> 3)
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def apply_brightness(color, brightness):
|
|
40
|
+
"""Apply brightness adjustment to RGB color.
|
|
41
|
+
|
|
42
|
+
Args:
|
|
43
|
+
color: Tuple of (r, g, b) values
|
|
44
|
+
brightness: Float from 0.0 to 1.0
|
|
45
|
+
|
|
46
|
+
Returns:
|
|
47
|
+
Tuple of brightness-adjusted (r, g, b) values
|
|
48
|
+
"""
|
|
49
|
+
return tuple(int(c * brightness) for c in color)
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
# Common color constants matching CircuitPython
|
|
53
|
+
BLACK = 0x0000
|
|
54
|
+
WHITE = 0xFFFF
|
|
55
|
+
RED = 0xF800
|
|
56
|
+
GREEN = 0x07E0
|
|
57
|
+
BLUE = 0x001F
|
|
58
|
+
CYAN = 0x07FF
|
|
59
|
+
MAGENTA = 0xF81F
|
|
60
|
+
YELLOW = 0xFFE0
|
|
61
|
+
ORANGE = 0xFD20
|
|
62
|
+
PURPLE = 0x8010
|
|
@@ -0,0 +1,254 @@
|
|
|
1
|
+
{
|
|
2
|
+
"board": "adafruit_matrixportal_s3",
|
|
3
|
+
"cp": "9.1.0",
|
|
4
|
+
"benchmarks": [
|
|
5
|
+
{
|
|
6
|
+
"name": "noop_loop_iter",
|
|
7
|
+
"category": "compute",
|
|
8
|
+
"value": 2018.74,
|
|
9
|
+
"note": "for-loop overhead (subtracted below)",
|
|
10
|
+
"iters": 20000,
|
|
11
|
+
"unit": "ns/op"
|
|
12
|
+
},
|
|
13
|
+
{
|
|
14
|
+
"name": "int_add",
|
|
15
|
+
"category": "compute",
|
|
16
|
+
"value": 1538.09,
|
|
17
|
+
"note": "",
|
|
18
|
+
"iters": 20000,
|
|
19
|
+
"unit": "ns/op"
|
|
20
|
+
},
|
|
21
|
+
{
|
|
22
|
+
"name": "int_mul",
|
|
23
|
+
"category": "compute",
|
|
24
|
+
"value": 1684.57,
|
|
25
|
+
"note": "",
|
|
26
|
+
"iters": 20000,
|
|
27
|
+
"unit": "ns/op"
|
|
28
|
+
},
|
|
29
|
+
{
|
|
30
|
+
"name": "float_add",
|
|
31
|
+
"category": "compute",
|
|
32
|
+
"value": 2069.09,
|
|
33
|
+
"note": "",
|
|
34
|
+
"iters": 20000,
|
|
35
|
+
"unit": "ns/op"
|
|
36
|
+
},
|
|
37
|
+
{
|
|
38
|
+
"name": "float_mul",
|
|
39
|
+
"category": "compute",
|
|
40
|
+
"value": 2066.04,
|
|
41
|
+
"note": "",
|
|
42
|
+
"iters": 20000,
|
|
43
|
+
"unit": "ns/op"
|
|
44
|
+
},
|
|
45
|
+
{
|
|
46
|
+
"name": "list_index",
|
|
47
|
+
"category": "compute",
|
|
48
|
+
"value": 2917.48,
|
|
49
|
+
"note": "",
|
|
50
|
+
"iters": 20000,
|
|
51
|
+
"unit": "ns/op"
|
|
52
|
+
},
|
|
53
|
+
{
|
|
54
|
+
"name": "dict_get",
|
|
55
|
+
"category": "compute",
|
|
56
|
+
"value": 2337.65,
|
|
57
|
+
"note": "",
|
|
58
|
+
"iters": 20000,
|
|
59
|
+
"unit": "ns/op"
|
|
60
|
+
},
|
|
61
|
+
{
|
|
62
|
+
"name": "func_call",
|
|
63
|
+
"category": "compute",
|
|
64
|
+
"value": 4661.56,
|
|
65
|
+
"note": "",
|
|
66
|
+
"iters": 20000,
|
|
67
|
+
"unit": "ns/op"
|
|
68
|
+
},
|
|
69
|
+
{
|
|
70
|
+
"name": "attr_access",
|
|
71
|
+
"category": "compute",
|
|
72
|
+
"value": 2259.83,
|
|
73
|
+
"note": "",
|
|
74
|
+
"iters": 20000,
|
|
75
|
+
"unit": "ns/op"
|
|
76
|
+
},
|
|
77
|
+
{
|
|
78
|
+
"name": "bitmap_setpixel",
|
|
79
|
+
"category": "pixel_interpreted",
|
|
80
|
+
"value": 6996.15,
|
|
81
|
+
"note": "bmp[x,y]=1",
|
|
82
|
+
"iters": 10000,
|
|
83
|
+
"unit": "ns/op"
|
|
84
|
+
},
|
|
85
|
+
{
|
|
86
|
+
"name": "bitmap_getpixel",
|
|
87
|
+
"category": "pixel_interpreted",
|
|
88
|
+
"value": 6690.98,
|
|
89
|
+
"note": "",
|
|
90
|
+
"iters": 10000,
|
|
91
|
+
"unit": "ns/op"
|
|
92
|
+
},
|
|
93
|
+
{
|
|
94
|
+
"name": "bitmap_fill_64x32",
|
|
95
|
+
"category": "bulk_c",
|
|
96
|
+
"value": 8926.39,
|
|
97
|
+
"note": "C fill 2048 px",
|
|
98
|
+
"iters": 4000,
|
|
99
|
+
"unit": "ns/call"
|
|
100
|
+
},
|
|
101
|
+
{
|
|
102
|
+
"name": "bitmaptools_blit_16x16",
|
|
103
|
+
"category": "bulk_c",
|
|
104
|
+
"value": 159462.0,
|
|
105
|
+
"note": "C blit 256 px",
|
|
106
|
+
"iters": 4000,
|
|
107
|
+
"unit": "ns/call"
|
|
108
|
+
},
|
|
109
|
+
{
|
|
110
|
+
"name": "bitmaptools_fill_region",
|
|
111
|
+
"category": "bulk_c",
|
|
112
|
+
"value": 147049.0,
|
|
113
|
+
"note": "512 px",
|
|
114
|
+
"iters": 4000,
|
|
115
|
+
"unit": "ns/call"
|
|
116
|
+
},
|
|
117
|
+
{
|
|
118
|
+
"name": "bitmaptools_draw_line",
|
|
119
|
+
"category": "bulk_c",
|
|
120
|
+
"value": 29617.3,
|
|
121
|
+
"note": "",
|
|
122
|
+
"iters": 4000,
|
|
123
|
+
"unit": "ns/call"
|
|
124
|
+
},
|
|
125
|
+
{
|
|
126
|
+
"name": "palette_set",
|
|
127
|
+
"category": "display_obj",
|
|
128
|
+
"value": 2629.09,
|
|
129
|
+
"note": "",
|
|
130
|
+
"iters": 10000,
|
|
131
|
+
"unit": "ns/op"
|
|
132
|
+
},
|
|
133
|
+
{
|
|
134
|
+
"name": "tilegrid_create",
|
|
135
|
+
"category": "display_obj",
|
|
136
|
+
"value": 47607.4,
|
|
137
|
+
"note": "alloc+init",
|
|
138
|
+
"iters": 500,
|
|
139
|
+
"unit": "ns/call"
|
|
140
|
+
},
|
|
141
|
+
{
|
|
142
|
+
"name": "tilegrid_move_x",
|
|
143
|
+
"category": "display_obj",
|
|
144
|
+
"value": 2491.76,
|
|
145
|
+
"note": "",
|
|
146
|
+
"iters": 10000,
|
|
147
|
+
"unit": "ns/op"
|
|
148
|
+
},
|
|
149
|
+
{
|
|
150
|
+
"name": "group_append_pop_pair",
|
|
151
|
+
"category": "display_obj",
|
|
152
|
+
"value": 10721.8,
|
|
153
|
+
"note": "",
|
|
154
|
+
"iters": 3000,
|
|
155
|
+
"unit": "ns/pair"
|
|
156
|
+
},
|
|
157
|
+
{
|
|
158
|
+
"name": "bitmap_alloc_32x16",
|
|
159
|
+
"category": "memory",
|
|
160
|
+
"value": 123291.0,
|
|
161
|
+
"note": "",
|
|
162
|
+
"iters": 1000,
|
|
163
|
+
"unit": "ns/alloc"
|
|
164
|
+
},
|
|
165
|
+
{
|
|
166
|
+
"name": "bytearray_alloc_256",
|
|
167
|
+
"category": "memory",
|
|
168
|
+
"value": 73989.8,
|
|
169
|
+
"note": "",
|
|
170
|
+
"iters": 2000,
|
|
171
|
+
"unit": "ns/alloc"
|
|
172
|
+
},
|
|
173
|
+
{
|
|
174
|
+
"name": "gc_collect",
|
|
175
|
+
"category": "memory",
|
|
176
|
+
"value": 899251.0,
|
|
177
|
+
"note": "clean heap",
|
|
178
|
+
"iters": 30,
|
|
179
|
+
"unit": "ns/call"
|
|
180
|
+
},
|
|
181
|
+
{
|
|
182
|
+
"name": "monotonic_ns_call",
|
|
183
|
+
"category": "io",
|
|
184
|
+
"value": 8848.57,
|
|
185
|
+
"note": "",
|
|
186
|
+
"iters": 10000,
|
|
187
|
+
"unit": "ns/op"
|
|
188
|
+
},
|
|
189
|
+
{
|
|
190
|
+
"name": "refresh_empty_bd1",
|
|
191
|
+
"category": "refresh",
|
|
192
|
+
"value": 14241.3,
|
|
193
|
+
"note": "bit_depth=1, empty group",
|
|
194
|
+
"iters": 30,
|
|
195
|
+
"unit": "ns/call"
|
|
196
|
+
},
|
|
197
|
+
{
|
|
198
|
+
"name": "refresh_full_bd1",
|
|
199
|
+
"category": "refresh",
|
|
200
|
+
"value": 4312130.0,
|
|
201
|
+
"note": "bit_depth=1, 64x32 recomposite",
|
|
202
|
+
"iters": 30,
|
|
203
|
+
"unit": "ns/call"
|
|
204
|
+
},
|
|
205
|
+
{
|
|
206
|
+
"name": "refresh_empty_bd2",
|
|
207
|
+
"category": "refresh",
|
|
208
|
+
"value": 15259.0,
|
|
209
|
+
"note": "bit_depth=2, empty group",
|
|
210
|
+
"iters": 30,
|
|
211
|
+
"unit": "ns/call"
|
|
212
|
+
},
|
|
213
|
+
{
|
|
214
|
+
"name": "refresh_full_bd2",
|
|
215
|
+
"category": "refresh",
|
|
216
|
+
"value": 4372150.0,
|
|
217
|
+
"note": "bit_depth=2, 64x32 recomposite",
|
|
218
|
+
"iters": 30,
|
|
219
|
+
"unit": "ns/call"
|
|
220
|
+
},
|
|
221
|
+
{
|
|
222
|
+
"name": "refresh_empty_bd4",
|
|
223
|
+
"category": "refresh",
|
|
224
|
+
"value": 17293.1,
|
|
225
|
+
"note": "bit_depth=4, empty group",
|
|
226
|
+
"iters": 30,
|
|
227
|
+
"unit": "ns/call"
|
|
228
|
+
},
|
|
229
|
+
{
|
|
230
|
+
"name": "refresh_full_bd4",
|
|
231
|
+
"category": "refresh",
|
|
232
|
+
"value": 4492190.0,
|
|
233
|
+
"note": "bit_depth=4, 64x32 recomposite",
|
|
234
|
+
"iters": 30,
|
|
235
|
+
"unit": "ns/call"
|
|
236
|
+
},
|
|
237
|
+
{
|
|
238
|
+
"name": "refresh_empty_bd6",
|
|
239
|
+
"category": "refresh",
|
|
240
|
+
"value": 14241.3,
|
|
241
|
+
"note": "bit_depth=6, empty group",
|
|
242
|
+
"iters": 30,
|
|
243
|
+
"unit": "ns/call"
|
|
244
|
+
},
|
|
245
|
+
{
|
|
246
|
+
"name": "refresh_full_bd6",
|
|
247
|
+
"category": "refresh",
|
|
248
|
+
"value": 13691200.0,
|
|
249
|
+
"note": "bit_depth=6, 64x32 recomposite",
|
|
250
|
+
"iters": 30,
|
|
251
|
+
"unit": "ns/call"
|
|
252
|
+
}
|
|
253
|
+
]
|
|
254
|
+
}
|
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
# Copyright (c) 2024-2026 Michael Winslow Czeiszperger
|
|
2
|
+
"""Turn accumulated modeled frame costs into a human/AI-readable feasibility report.
|
|
3
|
+
|
|
4
|
+
Answers: "would this app actually run on the real hardware?" — estimated hardware
|
|
5
|
+
FPS, where the per-frame time goes, estimated peak RAM vs budget, and actionable
|
|
6
|
+
warnings. Always labels itself as an estimate when the profile is uncalibrated.
|
|
7
|
+
|
|
8
|
+
Desktop/simulator only.
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
import math
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def _one_sig_fig(x):
|
|
15
|
+
"""Round to one significant figure (so '~5 FPS', not '5.37 FPS')."""
|
|
16
|
+
if x <= 0:
|
|
17
|
+
return 0
|
|
18
|
+
d = math.floor(math.log10(x))
|
|
19
|
+
factor = 10 ** d
|
|
20
|
+
return round(x / factor) * factor
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def _median(values):
|
|
24
|
+
s = sorted(values)
|
|
25
|
+
n = len(s)
|
|
26
|
+
if n == 0:
|
|
27
|
+
return 0.0
|
|
28
|
+
mid = n // 2
|
|
29
|
+
return s[mid] if n % 2 else (s[mid - 1] + s[mid]) / 2.0
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
class FeasibilityReport:
|
|
33
|
+
"""Estimated hardware performance for a run, built from a PerformanceManager."""
|
|
34
|
+
|
|
35
|
+
def __init__(self, profile_name, confidence, source, calibrated,
|
|
36
|
+
est_hw_fps, median_frame_ms, worst_frame_ms, breakdown_ms,
|
|
37
|
+
est_peak_ram_bytes, ram_budget_bytes, warnings):
|
|
38
|
+
self.profile_name = profile_name
|
|
39
|
+
self.confidence = confidence
|
|
40
|
+
self.source = source
|
|
41
|
+
self.calibrated = calibrated
|
|
42
|
+
self.est_hw_fps = est_hw_fps # None if no frames were rendered
|
|
43
|
+
self.median_frame_ms = median_frame_ms
|
|
44
|
+
self.worst_frame_ms = worst_frame_ms
|
|
45
|
+
self.breakdown_ms = breakdown_ms # {category: ms} averaged per frame
|
|
46
|
+
self.est_peak_ram_bytes = est_peak_ram_bytes
|
|
47
|
+
self.ram_budget_bytes = ram_budget_bytes
|
|
48
|
+
self.warnings = warnings
|
|
49
|
+
|
|
50
|
+
# ------------------------------------------------------------------
|
|
51
|
+
@classmethod
|
|
52
|
+
def from_manager(cls, manager):
|
|
53
|
+
profile = manager.profile
|
|
54
|
+
frames = manager.frames
|
|
55
|
+
n = len(frames)
|
|
56
|
+
|
|
57
|
+
if n == 0:
|
|
58
|
+
return cls(profile.name, profile.confidence, profile.source,
|
|
59
|
+
profile.is_calibrated, None, 0.0, 0.0, {},
|
|
60
|
+
manager.estimated_peak_ram_bytes(), profile.usable_ram_bytes,
|
|
61
|
+
["No frames rendered yet — run the app (and call show()) first."])
|
|
62
|
+
|
|
63
|
+
totals_us = [f.total_us for f in frames]
|
|
64
|
+
median_us = _median(totals_us)
|
|
65
|
+
worst_us = max(totals_us)
|
|
66
|
+
est_fps = (1_000_000.0 / median_us) if median_us > 0 else None
|
|
67
|
+
|
|
68
|
+
# Average per-category breakdown (ms), and which category dominates.
|
|
69
|
+
cats = ("bitmap_rebuild_us", "refresh_us", "pixel_writes_us", "gc_us",
|
|
70
|
+
"bulk_ops_us")
|
|
71
|
+
breakdown_ms = {}
|
|
72
|
+
for cat in cats:
|
|
73
|
+
avg_us = sum(getattr(f, cat) for f in frames) / n
|
|
74
|
+
breakdown_ms[cat.replace("_us", "")] = avg_us / 1000.0
|
|
75
|
+
dominant = max(breakdown_ms, key=lambda k: breakdown_ms[k])
|
|
76
|
+
|
|
77
|
+
peak_ram = manager.estimated_peak_ram_bytes()
|
|
78
|
+
budget = profile.usable_ram_bytes
|
|
79
|
+
|
|
80
|
+
warnings = []
|
|
81
|
+
if est_fps is not None and est_fps < 1:
|
|
82
|
+
warnings.append(
|
|
83
|
+
"Frame would take ~%d ms on hardware (~%s FPS) — effectively frozen."
|
|
84
|
+
% (round(median_us / 1000.0), _fps_text(est_fps, profile.is_calibrated)))
|
|
85
|
+
elif est_fps is not None and est_fps < 10:
|
|
86
|
+
warnings.append(
|
|
87
|
+
"Frame would take ~%d ms on hardware (~%s FPS%s). Scrolling will stutter."
|
|
88
|
+
% (round(median_us / 1000.0), _fps_text(est_fps, profile.is_calibrated),
|
|
89
|
+
"" if profile.is_calibrated else ", ESTIMATE"))
|
|
90
|
+
if dominant == "bitmap_rebuild" and breakdown_ms["bitmap_rebuild"] > 0 \
|
|
91
|
+
and breakdown_ms["bitmap_rebuild"] >= 0.5 * (median_us / 1000.0):
|
|
92
|
+
warnings.append(
|
|
93
|
+
"draw_text rebuilds a glyph bitmap every frame (the dominant cost on "
|
|
94
|
+
"hardware). Cache the Label/content and only change .text when it "
|
|
95
|
+
"actually changes, instead of redrawing every frame.")
|
|
96
|
+
if peak_ram > budget:
|
|
97
|
+
warnings.append(
|
|
98
|
+
"Estimated peak RAM ~%d KB exceeds the modeled device budget of %d KB "
|
|
99
|
+
"— won't fit. Reduce content/effects or disable the web server."
|
|
100
|
+
% (peak_ram // 1024, budget // 1024))
|
|
101
|
+
elif peak_ram > 0.8 * budget:
|
|
102
|
+
warnings.append(
|
|
103
|
+
"Estimated peak RAM ~%d KB is close to the %d KB budget — little "
|
|
104
|
+
"headroom on hardware." % (peak_ram // 1024, budget // 1024))
|
|
105
|
+
|
|
106
|
+
return cls(profile.name, profile.confidence, profile.source,
|
|
107
|
+
profile.is_calibrated, est_fps, median_us / 1000.0,
|
|
108
|
+
worst_us / 1000.0, breakdown_ms, peak_ram, budget, warnings)
|
|
109
|
+
|
|
110
|
+
# ------------------------------------------------------------------
|
|
111
|
+
def as_dict(self):
|
|
112
|
+
return {
|
|
113
|
+
"profile": self.profile_name,
|
|
114
|
+
"confidence": self.confidence,
|
|
115
|
+
"calibrated": self.calibrated,
|
|
116
|
+
"source": self.source,
|
|
117
|
+
"estimated_hardware_fps": self.est_hw_fps,
|
|
118
|
+
"median_frame_ms": round(self.median_frame_ms, 2),
|
|
119
|
+
"worst_frame_ms": round(self.worst_frame_ms, 2),
|
|
120
|
+
"breakdown_ms": {k: round(v, 2) for k, v in self.breakdown_ms.items()},
|
|
121
|
+
"estimated_peak_ram_bytes": self.est_peak_ram_bytes,
|
|
122
|
+
"ram_budget_bytes": self.ram_budget_bytes,
|
|
123
|
+
"warnings": list(self.warnings),
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
def as_text(self):
|
|
127
|
+
lines = ["=== Hardware feasibility: %s ===" % self.profile_name]
|
|
128
|
+
if self.calibrated:
|
|
129
|
+
lines.append(" Confidence: MEASURED on device (%s)" % self.source)
|
|
130
|
+
else:
|
|
131
|
+
lines.append(" Confidence: ROUGH ESTIMATE, not measured on device (%s)"
|
|
132
|
+
% self.source)
|
|
133
|
+
if self.est_hw_fps is None:
|
|
134
|
+
lines.append(" Estimated hardware FPS: n/a (no frames rendered)")
|
|
135
|
+
else:
|
|
136
|
+
lines.append(" Estimated hardware FPS: ~%s (median frame ~%d ms, worst ~%d ms)"
|
|
137
|
+
% (_fps_text(self.est_hw_fps, self.calibrated),
|
|
138
|
+
round(self.median_frame_ms), round(self.worst_frame_ms)))
|
|
139
|
+
if self.breakdown_ms:
|
|
140
|
+
parts = sorted(self.breakdown_ms.items(), key=lambda kv: -kv[1])
|
|
141
|
+
lines.append(" Per-frame cost (avg): "
|
|
142
|
+
+ " | ".join("%s %.1f ms" % (k, v) for k, v in parts))
|
|
143
|
+
lines.append(" Estimated peak RAM: %d KB / %d KB budget"
|
|
144
|
+
% (self.est_peak_ram_bytes // 1024, self.ram_budget_bytes // 1024))
|
|
145
|
+
if self.warnings:
|
|
146
|
+
lines.append(" Warnings:")
|
|
147
|
+
for w in self.warnings:
|
|
148
|
+
lines.append(" - " + w)
|
|
149
|
+
else:
|
|
150
|
+
lines.append(" No feasibility warnings.")
|
|
151
|
+
return "\n".join(lines)
|
|
152
|
+
|
|
153
|
+
|
|
154
|
+
def _fps_text(fps, calibrated):
|
|
155
|
+
if fps is None:
|
|
156
|
+
return "n/a"
|
|
157
|
+
if calibrated:
|
|
158
|
+
return "%.1f" % fps
|
|
159
|
+
sig = _one_sig_fig(fps)
|
|
160
|
+
return ("%g" % sig)
|