mini-arcade-core 1.0.2__py3-none-any.whl → 1.1.1__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.
- mini_arcade_core/backend/backend.py +33 -0
- mini_arcade_core/engine/commands.py +25 -2
- mini_arcade_core/engine/game.py +88 -8
- mini_arcade_core/engine/render/context.py +48 -0
- mini_arcade_core/engine/render/effects/__init__.py +0 -0
- mini_arcade_core/engine/render/effects/base.py +88 -0
- mini_arcade_core/engine/render/effects/crt.py +68 -0
- mini_arcade_core/engine/render/effects/registry.py +50 -0
- mini_arcade_core/engine/render/effects/vignette.py +79 -0
- mini_arcade_core/engine/render/frame_packet.py +26 -0
- mini_arcade_core/engine/render/passes/__init__.py +0 -0
- mini_arcade_core/engine/render/passes/base.py +37 -0
- mini_arcade_core/engine/render/passes/begin_frame.py +27 -0
- mini_arcade_core/engine/render/passes/end_frame.py +28 -0
- mini_arcade_core/engine/render/passes/lighting.py +28 -0
- mini_arcade_core/engine/render/passes/postfx.py +49 -0
- mini_arcade_core/engine/render/passes/ui.py +41 -0
- mini_arcade_core/engine/render/passes/world.py +55 -0
- mini_arcade_core/engine/render/pipeline.py +60 -11
- mini_arcade_core/engine/render/render_service.py +22 -0
- mini_arcade_core/runtime/render/__init__.py +0 -0
- mini_arcade_core/runtime/render/render_port.py +22 -0
- mini_arcade_core/runtime/scene/scene_adapter.py +1 -1
- mini_arcade_core/runtime/scene/scene_port.py +1 -1
- mini_arcade_core/runtime/services.py +2 -0
- mini_arcade_core/scenes/debug_overlay.py +4 -1
- mini_arcade_core/scenes/registry.py +1 -2
- {mini_arcade_core-1.0.2.dist-info → mini_arcade_core-1.1.1.dist-info}/METADATA +1 -1
- {mini_arcade_core-1.0.2.dist-info → mini_arcade_core-1.1.1.dist-info}/RECORD +31 -13
- {mini_arcade_core-1.0.2.dist-info → mini_arcade_core-1.1.1.dist-info}/WHEEL +0 -0
- {mini_arcade_core-1.0.2.dist-info → mini_arcade_core-1.1.1.dist-info}/licenses/LICENSE +0 -0
|
@@ -289,3 +289,36 @@ class Backend(Protocol):
|
|
|
289
289
|
def clear_clip_rect(self):
|
|
290
290
|
"""Clear any clipping rectangle."""
|
|
291
291
|
raise NotImplementedError
|
|
292
|
+
|
|
293
|
+
# Justification: Simple drawing API for now
|
|
294
|
+
# pylint: disable=too-many-arguments,too-many-positional-arguments
|
|
295
|
+
def draw_line(
|
|
296
|
+
self,
|
|
297
|
+
x1: int,
|
|
298
|
+
y1: int,
|
|
299
|
+
x2: int,
|
|
300
|
+
y2: int,
|
|
301
|
+
color: tuple[int, ...] = (255, 255, 255),
|
|
302
|
+
):
|
|
303
|
+
"""
|
|
304
|
+
Draw a line between two points in some default color.
|
|
305
|
+
|
|
306
|
+
:param x1: X position of the start point.
|
|
307
|
+
:type x1: int
|
|
308
|
+
|
|
309
|
+
:param y1: Y position of the start point.
|
|
310
|
+
:type y1: int
|
|
311
|
+
|
|
312
|
+
:param x2: X position of the end point.
|
|
313
|
+
:type x2: int
|
|
314
|
+
|
|
315
|
+
:param y2: Y position of the end point.
|
|
316
|
+
:type y2: int
|
|
317
|
+
|
|
318
|
+
:param color: RGB color tuple.
|
|
319
|
+
:type color: tuple[int, ...]
|
|
320
|
+
"""
|
|
321
|
+
raise NotImplementedError
|
|
322
|
+
|
|
323
|
+
|
|
324
|
+
# pylint: enable=too-many-arguments,too-many-positional-arguments
|
|
@@ -173,11 +173,15 @@ class ChangeSceneCommand(Command):
|
|
|
173
173
|
|
|
174
174
|
@dataclass(frozen=True)
|
|
175
175
|
class ToggleDebugOverlayCommand(Command):
|
|
176
|
-
"""
|
|
176
|
+
"""
|
|
177
|
+
Toggle the debug overlay scene.
|
|
178
|
+
|
|
179
|
+
:cvar DEBUG_OVERLAY_ID: str: Identifier for the debug overlay scene.
|
|
180
|
+
"""
|
|
177
181
|
|
|
178
182
|
DEBUG_OVERLAY_ID = "debug_overlay"
|
|
179
183
|
|
|
180
|
-
def execute(self, context: CommandContext)
|
|
184
|
+
def execute(self, context: CommandContext):
|
|
181
185
|
scenes = context.services.scenes
|
|
182
186
|
if scenes.has_scene(self.DEBUG_OVERLAY_ID):
|
|
183
187
|
scenes.remove_scene(self.DEBUG_OVERLAY_ID)
|
|
@@ -193,3 +197,22 @@ class ToggleDebugOverlayCommand(Command):
|
|
|
193
197
|
receives_input=False,
|
|
194
198
|
),
|
|
195
199
|
)
|
|
200
|
+
|
|
201
|
+
|
|
202
|
+
@dataclass(frozen=True)
|
|
203
|
+
class ToggleEffectCommand(Command):
|
|
204
|
+
"""
|
|
205
|
+
Toggle a post-processing effect on or off.
|
|
206
|
+
|
|
207
|
+
:ivar effect_id (str): Identifier of the effect to toggle.
|
|
208
|
+
"""
|
|
209
|
+
|
|
210
|
+
effect_id: str
|
|
211
|
+
|
|
212
|
+
def execute(self, context: CommandContext) -> None:
|
|
213
|
+
# effects live in context.meta OR in a dedicated service/settings.
|
|
214
|
+
# v1 simplest: stash stack into context.settings or context.services.render
|
|
215
|
+
stack = getattr(context.settings, "effects_stack", None)
|
|
216
|
+
if stack is None:
|
|
217
|
+
return
|
|
218
|
+
stack.toggle(self.effect_id)
|
mini_arcade_core/engine/game.py
CHANGED
|
@@ -16,9 +16,20 @@ from mini_arcade_core.engine.commands import (
|
|
|
16
16
|
CommandQueue,
|
|
17
17
|
QuitCommand,
|
|
18
18
|
ToggleDebugOverlayCommand,
|
|
19
|
+
ToggleEffectCommand,
|
|
19
20
|
)
|
|
21
|
+
from mini_arcade_core.engine.render.context import RenderContext
|
|
22
|
+
from mini_arcade_core.engine.render.effects.base import (
|
|
23
|
+
EffectParams,
|
|
24
|
+
EffectStack,
|
|
25
|
+
)
|
|
26
|
+
from mini_arcade_core.engine.render.effects.crt import CRTEffect
|
|
27
|
+
from mini_arcade_core.engine.render.effects.registry import EffectRegistry
|
|
28
|
+
from mini_arcade_core.engine.render.effects.vignette import VignetteNoiseEffect
|
|
29
|
+
from mini_arcade_core.engine.render.frame_packet import FramePacket
|
|
20
30
|
from mini_arcade_core.engine.render.packet import RenderPacket
|
|
21
31
|
from mini_arcade_core.engine.render.pipeline import RenderPipeline
|
|
32
|
+
from mini_arcade_core.engine.render.render_service import RenderService
|
|
22
33
|
from mini_arcade_core.managers.cheats import CheatManager
|
|
23
34
|
from mini_arcade_core.runtime.audio.audio_adapter import SDLAudioAdapter
|
|
24
35
|
from mini_arcade_core.runtime.capture.capture_adapter import CaptureAdapter
|
|
@@ -49,6 +60,19 @@ class WindowConfig:
|
|
|
49
60
|
title: str
|
|
50
61
|
|
|
51
62
|
|
|
63
|
+
@dataclass
|
|
64
|
+
class PostFXConfig:
|
|
65
|
+
"""
|
|
66
|
+
Configuration for post-processing effects.
|
|
67
|
+
|
|
68
|
+
:ivar enabled (bool): Whether post effects are enabled by default.
|
|
69
|
+
:ivar active (list[str]): List of active effect IDs by default.
|
|
70
|
+
"""
|
|
71
|
+
|
|
72
|
+
enabled: bool = True
|
|
73
|
+
active: list[str] = field(default_factory=list)
|
|
74
|
+
|
|
75
|
+
|
|
52
76
|
@dataclass
|
|
53
77
|
class GameConfig:
|
|
54
78
|
"""
|
|
@@ -62,6 +86,7 @@ class GameConfig:
|
|
|
62
86
|
window: WindowConfig | None = None
|
|
63
87
|
fps: int = 60
|
|
64
88
|
backend: Backend | None = None
|
|
89
|
+
postfx: PostFXConfig = field(default_factory=PostFXConfig)
|
|
65
90
|
|
|
66
91
|
|
|
67
92
|
Difficulty = Literal["easy", "normal", "hard", "insane"]
|
|
@@ -76,6 +101,7 @@ class GameSettings:
|
|
|
76
101
|
"""
|
|
77
102
|
|
|
78
103
|
difficulty: Difficulty = "normal"
|
|
104
|
+
effects_stack: EffectStack | None = None
|
|
79
105
|
|
|
80
106
|
|
|
81
107
|
def _neutral_input(frame_index: int, dt: float) -> InputFrame:
|
|
@@ -188,6 +214,7 @@ class Game:
|
|
|
188
214
|
files=LocalFilesAdapter(),
|
|
189
215
|
capture=CaptureAdapter(self.backend),
|
|
190
216
|
input=InputAdapter(),
|
|
217
|
+
render=RenderService(),
|
|
191
218
|
)
|
|
192
219
|
|
|
193
220
|
self.command_queue = CommandQueue()
|
|
@@ -200,6 +227,9 @@ class Game:
|
|
|
200
227
|
# TODO: Fix too-many-statements and too-many-locals warnings
|
|
201
228
|
# Justification: Main game loop with multiple responsibilities.
|
|
202
229
|
# pylint: disable=too-many-statements,too-many-locals
|
|
230
|
+
# TODO: Fix too-many-branches warning
|
|
231
|
+
# Justification: Complex control flow in main loop.
|
|
232
|
+
# pylint: disable=too-many-branches
|
|
203
233
|
def run(self, initial_scene_id: str):
|
|
204
234
|
"""
|
|
205
235
|
Run the main loop starting with the given scene.
|
|
@@ -218,6 +248,26 @@ class Game:
|
|
|
218
248
|
|
|
219
249
|
pipeline = RenderPipeline()
|
|
220
250
|
|
|
251
|
+
effects_registry = EffectRegistry()
|
|
252
|
+
effects_registry.register(CRTEffect())
|
|
253
|
+
effects_registry.register(VignetteNoiseEffect())
|
|
254
|
+
|
|
255
|
+
effects_stack = EffectStack(
|
|
256
|
+
enabled=self.config.postfx.enabled,
|
|
257
|
+
active=list(self.config.postfx.active),
|
|
258
|
+
params={
|
|
259
|
+
"crt": EffectParams(intensity=0.35, wobble_speed=1.0),
|
|
260
|
+
"vignette_noise": EffectParams(
|
|
261
|
+
intensity=0.25, wobble_speed=1.0
|
|
262
|
+
),
|
|
263
|
+
},
|
|
264
|
+
)
|
|
265
|
+
self.settings.effects_stack = effects_stack
|
|
266
|
+
|
|
267
|
+
for p in pipeline.passes:
|
|
268
|
+
if getattr(p, "name", "") == "PostFXPass":
|
|
269
|
+
p.registry = effects_registry
|
|
270
|
+
|
|
221
271
|
self._running = True
|
|
222
272
|
target_dt = 1.0 / self.config.fps if self.config.fps > 0 else 0.0
|
|
223
273
|
last_time = perf_counter()
|
|
@@ -234,6 +284,7 @@ class Game:
|
|
|
234
284
|
# & self.services.scenes.input_entry
|
|
235
285
|
# Justification: These methods are expected to return values.
|
|
236
286
|
# pylint: disable=assignment-from-no-return
|
|
287
|
+
time_s = 0.0
|
|
237
288
|
|
|
238
289
|
while self._running:
|
|
239
290
|
timer.clear()
|
|
@@ -251,8 +302,17 @@ class Game:
|
|
|
251
302
|
logger.debug(f"Window resized event: {w}x{h}")
|
|
252
303
|
self.services.window.on_window_resized(w, h)
|
|
253
304
|
# if F1 pressed, toggle debug overlay
|
|
254
|
-
if e.type == EventType.KEYDOWN
|
|
255
|
-
|
|
305
|
+
if e.type == EventType.KEYDOWN:
|
|
306
|
+
if e.key == Key.F1:
|
|
307
|
+
self.command_queue.push(ToggleDebugOverlayCommand())
|
|
308
|
+
elif e.key == Key.F2:
|
|
309
|
+
self.command_queue.push(ToggleEffectCommand("crt"))
|
|
310
|
+
elif e.key == Key.F3:
|
|
311
|
+
self.command_queue.push(
|
|
312
|
+
ToggleEffectCommand("vignette_noise")
|
|
313
|
+
)
|
|
314
|
+
elif e.key == Key.F4:
|
|
315
|
+
effects_stack.enabled = not effects_stack.enabled
|
|
256
316
|
timer.mark("events_polled")
|
|
257
317
|
|
|
258
318
|
input_frame = self.services.input.build(events, frame_index, dt)
|
|
@@ -304,23 +364,43 @@ class Game:
|
|
|
304
364
|
cmd.execute(command_context)
|
|
305
365
|
timer.mark("cmd_exec_end")
|
|
306
366
|
|
|
367
|
+
# ---------------- TO REPLACE WITH RENDERING PIPELINE ----------------
|
|
307
368
|
timer.mark("render_start")
|
|
308
|
-
backend.begin_frame()
|
|
309
|
-
timer.mark("begin_frame_done")
|
|
310
369
|
|
|
311
370
|
vp = self.services.window.get_viewport()
|
|
371
|
+
|
|
372
|
+
# gather visible packets
|
|
373
|
+
frame_packets: list[RenderPacket] = []
|
|
312
374
|
for entry in self.services.scenes.visible_entries():
|
|
313
375
|
scene = entry.scene
|
|
314
376
|
packet = packet_cache.get(id(scene))
|
|
315
377
|
if packet is None:
|
|
316
|
-
# bootstrap (first frame visible but not updated)
|
|
317
378
|
packet = scene.tick(_neutral_input(frame_index, 0.0), 0.0)
|
|
318
379
|
packet_cache[id(scene)] = packet
|
|
380
|
+
frame_packets.append(
|
|
381
|
+
FramePacket(
|
|
382
|
+
scene_id=entry.scene_id,
|
|
383
|
+
is_overlay=entry.is_overlay,
|
|
384
|
+
packet=packet,
|
|
385
|
+
)
|
|
386
|
+
)
|
|
387
|
+
|
|
388
|
+
render_ctx = RenderContext(
|
|
389
|
+
viewport=vp,
|
|
390
|
+
debug_overlay=getattr(self.settings, "debug_overlay", False),
|
|
391
|
+
frame_ms=dt * 1000.0,
|
|
392
|
+
)
|
|
393
|
+
time_s += dt
|
|
394
|
+
render_ctx.meta["frame_index"] = frame_index
|
|
395
|
+
render_ctx.meta["time_s"] = time_s
|
|
396
|
+
render_ctx.meta["effects_stack"] = effects_stack
|
|
319
397
|
|
|
320
|
-
|
|
398
|
+
self.services.render.last_frame_ms = render_ctx.frame_ms
|
|
399
|
+
self.services.render.last_stats = render_ctx.stats
|
|
400
|
+
pipeline.render_frame(backend, render_ctx, frame_packets)
|
|
321
401
|
|
|
322
|
-
timer.mark("
|
|
323
|
-
|
|
402
|
+
timer.mark("render_done")
|
|
403
|
+
# ---------------- END RENDERING PIPELINE ----------------------------
|
|
324
404
|
timer.mark("end_frame_done")
|
|
325
405
|
|
|
326
406
|
timer.mark("sleep_start")
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Render context and stats for a single frame render.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
from __future__ import annotations
|
|
6
|
+
|
|
7
|
+
from dataclasses import dataclass, field
|
|
8
|
+
from typing import Any
|
|
9
|
+
|
|
10
|
+
from mini_arcade_core.engine.render.viewport import ViewportState
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
@dataclass
|
|
14
|
+
class RenderStats:
|
|
15
|
+
"""
|
|
16
|
+
Statistics about the rendering process for a single frame.
|
|
17
|
+
|
|
18
|
+
:ivar packets (int): Number of render packets processed.
|
|
19
|
+
:ivar ops (int): Number of rendering operations executed.
|
|
20
|
+
:ivar draw_groups (int): Number of draw groups processed.
|
|
21
|
+
:ivar renderables (int): Number of renderable objects processed.
|
|
22
|
+
:ivar draw_groups (int): Number of draw groups processed.
|
|
23
|
+
"""
|
|
24
|
+
|
|
25
|
+
packets: int = 0
|
|
26
|
+
ops: int = 0
|
|
27
|
+
draw_groups: int = 0 # approx ok
|
|
28
|
+
renderables: int = 0
|
|
29
|
+
draw_groups: int = 0
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
@dataclass
|
|
33
|
+
class RenderContext:
|
|
34
|
+
"""
|
|
35
|
+
Context for rendering a single frame.
|
|
36
|
+
|
|
37
|
+
:ivar viewport: ViewportState: Current viewport state.
|
|
38
|
+
:ivar debug_overlay: bool: Whether to render debug overlays.
|
|
39
|
+
:ivar frame_ms: float: Time taken to render the frame in milliseconds.
|
|
40
|
+
:ivar stats: RenderStats: Statistics about the rendering process.
|
|
41
|
+
:ivar meta: dict[str, Any]: Additional metadata for rendering.
|
|
42
|
+
"""
|
|
43
|
+
|
|
44
|
+
viewport: ViewportState
|
|
45
|
+
debug_overlay: bool = False
|
|
46
|
+
frame_ms: float = 0.0
|
|
47
|
+
stats: RenderStats = field(default_factory=RenderStats)
|
|
48
|
+
meta: dict[str, Any] = field(default_factory=dict)
|
|
File without changes
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Screen-space post effects base classes and protocols.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
from __future__ import annotations
|
|
6
|
+
|
|
7
|
+
from dataclasses import dataclass, field
|
|
8
|
+
from typing import Protocol, runtime_checkable
|
|
9
|
+
|
|
10
|
+
from mini_arcade_core.backend import Backend
|
|
11
|
+
from mini_arcade_core.engine.render.context import RenderContext
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
@runtime_checkable
|
|
15
|
+
class Effect(Protocol):
|
|
16
|
+
"""
|
|
17
|
+
Screen-space post effect.
|
|
18
|
+
|
|
19
|
+
IMPORTANT: Effects should draw ONLY using ctx.viewport (screen-space),
|
|
20
|
+
and must not assume anything about world-space transforms.
|
|
21
|
+
"""
|
|
22
|
+
|
|
23
|
+
effect_id: str
|
|
24
|
+
|
|
25
|
+
def apply(self, backend: Backend, ctx: RenderContext) -> None:
|
|
26
|
+
"""
|
|
27
|
+
Apply the effect to the current framebuffer.
|
|
28
|
+
|
|
29
|
+
:param backend: Backend to use for rendering.
|
|
30
|
+
:type backend: Backend
|
|
31
|
+
|
|
32
|
+
:param ctx: Render context with viewport info.
|
|
33
|
+
:type ctx: RenderContext
|
|
34
|
+
"""
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
@dataclass
|
|
38
|
+
class EffectParams:
|
|
39
|
+
"""
|
|
40
|
+
Shared params (Material-ish controls) for v1.
|
|
41
|
+
|
|
42
|
+
:ivar intensity (float): Effect intensity.
|
|
43
|
+
:ivar wobble_speed (float): Speed factor for animated distortion.
|
|
44
|
+
:ivar tint (tuple[int, int, int, int] | None): Optional RGBA tint.
|
|
45
|
+
"""
|
|
46
|
+
|
|
47
|
+
intensity: float = 1.0
|
|
48
|
+
wobble_speed: float = 1.0
|
|
49
|
+
tint: tuple[int, int, int, int] | None = None
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
@dataclass
|
|
53
|
+
class EffectStack:
|
|
54
|
+
"""
|
|
55
|
+
Runtime state: what effects are enabled + their params.
|
|
56
|
+
|
|
57
|
+
Zero-overhead path:
|
|
58
|
+
- if enabled=False OR active is empty => PostFXPass returns immediately.
|
|
59
|
+
|
|
60
|
+
:ivar enabled (bool): Master toggle for post effects.
|
|
61
|
+
:ivar active (list[str]): List of active effect IDs.
|
|
62
|
+
:ivar params (dict[str, EffectParams]): Per-effect parameters.
|
|
63
|
+
"""
|
|
64
|
+
|
|
65
|
+
enabled: bool = False
|
|
66
|
+
active: list[str] = field(default_factory=list)
|
|
67
|
+
params: dict[str, EffectParams] = field(default_factory=dict)
|
|
68
|
+
|
|
69
|
+
def is_active(self) -> bool:
|
|
70
|
+
"""
|
|
71
|
+
Check if any effects are active.
|
|
72
|
+
|
|
73
|
+
:return: True if effects are enabled and at least one is active.
|
|
74
|
+
:rtype: bool
|
|
75
|
+
"""
|
|
76
|
+
return self.enabled and bool(self.active)
|
|
77
|
+
|
|
78
|
+
def toggle(self, effect_id: str) -> None:
|
|
79
|
+
"""
|
|
80
|
+
Toggle an effect on/off.
|
|
81
|
+
|
|
82
|
+
:param effect_id: ID of the effect to toggle.
|
|
83
|
+
:type effect_id: str
|
|
84
|
+
"""
|
|
85
|
+
if effect_id in self.active:
|
|
86
|
+
self.active.remove(effect_id)
|
|
87
|
+
else:
|
|
88
|
+
self.active.append(effect_id)
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
"""
|
|
2
|
+
CRT screen-space post effect.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
# Justification: PoC code for v1.
|
|
6
|
+
# pylint: disable=duplicate-code
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
from dataclasses import dataclass
|
|
11
|
+
from math import sin
|
|
12
|
+
|
|
13
|
+
from mini_arcade_core.backend import Backend
|
|
14
|
+
from mini_arcade_core.engine.render.context import RenderContext
|
|
15
|
+
from mini_arcade_core.engine.render.effects.base import EffectParams
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
@dataclass
|
|
19
|
+
class CRTEffect:
|
|
20
|
+
"""
|
|
21
|
+
CRT screen-space post effect.
|
|
22
|
+
Simulates CRT scanlines with optional wobble.
|
|
23
|
+
"""
|
|
24
|
+
|
|
25
|
+
effect_id: str = "crt"
|
|
26
|
+
|
|
27
|
+
# Justification: This is PoC code for v1.
|
|
28
|
+
# pylint: disable=too-many-locals
|
|
29
|
+
def apply(self, backend: Backend, ctx: RenderContext) -> None:
|
|
30
|
+
"""Apply the CRT effect to the current render context."""
|
|
31
|
+
vp = ctx.viewport
|
|
32
|
+
x0, y0 = vp.offset_x, vp.offset_y
|
|
33
|
+
w, h = vp.viewport_w, vp.viewport_h
|
|
34
|
+
|
|
35
|
+
stack = ctx.meta.get("effects_stack")
|
|
36
|
+
params: EffectParams = (
|
|
37
|
+
stack.params.get(self.effect_id, EffectParams())
|
|
38
|
+
if stack
|
|
39
|
+
else EffectParams()
|
|
40
|
+
)
|
|
41
|
+
|
|
42
|
+
intensity = max(0.0, min(1.0, params.intensity))
|
|
43
|
+
if intensity <= 0.0:
|
|
44
|
+
return
|
|
45
|
+
|
|
46
|
+
# Use a time value from ctx.meta (added in Game.run below)
|
|
47
|
+
t = float(ctx.meta.get("time_s", 0.0))
|
|
48
|
+
wobble = float(params.wobble_speed)
|
|
49
|
+
|
|
50
|
+
# Clip to viewport so it works with all viewport modes/resolutions
|
|
51
|
+
backend.set_clip_rect(x0, y0, w, h)
|
|
52
|
+
|
|
53
|
+
# Scanlines: draw every N lines with low alpha
|
|
54
|
+
# Note: assumes Backend supports alpha in color tuples.
|
|
55
|
+
spacing = 2 # tweakable
|
|
56
|
+
base_alpha = 120 # int(40 * intensity) # subtle
|
|
57
|
+
line_color = (255, 255, 255, base_alpha)
|
|
58
|
+
|
|
59
|
+
# "Wobble": tiny horizontal shift that animates over time
|
|
60
|
+
# Keep it tiny to avoid looking like a bug.
|
|
61
|
+
for y in range(y0, y0 + h, spacing):
|
|
62
|
+
# shift in pixels, -2..2-ish
|
|
63
|
+
shift = int(2.0 * intensity * sin((y * 0.05) + (t * wobble)))
|
|
64
|
+
backend.draw_line(
|
|
65
|
+
x0 + shift, y, x0 + w + shift, y, color=line_color
|
|
66
|
+
)
|
|
67
|
+
|
|
68
|
+
backend.clear_clip_rect()
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Screen-space post effects registry.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
from __future__ import annotations
|
|
6
|
+
|
|
7
|
+
from dataclasses import dataclass, field
|
|
8
|
+
|
|
9
|
+
from mini_arcade_core.engine.render.effects.base import Effect
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
@dataclass
|
|
13
|
+
class EffectRegistry:
|
|
14
|
+
"""
|
|
15
|
+
Registry of available screen-space post effects.
|
|
16
|
+
|
|
17
|
+
:ivar _effects: dict[str, Effect]: Internal mapping of effect IDs to effects.
|
|
18
|
+
"""
|
|
19
|
+
|
|
20
|
+
_effects: dict[str, Effect] = field(default_factory=dict)
|
|
21
|
+
|
|
22
|
+
def register(self, effect: Effect) -> None:
|
|
23
|
+
"""
|
|
24
|
+
Register a new effect in the registry.
|
|
25
|
+
|
|
26
|
+
:param effect: Effect to register.
|
|
27
|
+
:type effect: Effect
|
|
28
|
+
"""
|
|
29
|
+
self._effects[effect.effect_id] = effect
|
|
30
|
+
|
|
31
|
+
def get(self, effect_id: str) -> Effect | None:
|
|
32
|
+
"""
|
|
33
|
+
Get an effect by its ID.
|
|
34
|
+
|
|
35
|
+
:param effect_id: ID of the effect to retrieve.
|
|
36
|
+
:type effect_id: str
|
|
37
|
+
|
|
38
|
+
:return: Effect instance or None if not found.
|
|
39
|
+
:rtype: Effect | None
|
|
40
|
+
"""
|
|
41
|
+
return self._effects.get(effect_id)
|
|
42
|
+
|
|
43
|
+
def all_ids(self) -> list[str]:
|
|
44
|
+
"""
|
|
45
|
+
Get a list of all registered effect IDs.
|
|
46
|
+
|
|
47
|
+
:return: List of effect IDs.
|
|
48
|
+
:rtype: list[str]
|
|
49
|
+
"""
|
|
50
|
+
return list(self._effects.keys())
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Vignette noise screen-space post effect.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
# Justification: PoC code for v1.
|
|
6
|
+
# pylint: disable=duplicate-code
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
import random
|
|
11
|
+
from dataclasses import dataclass
|
|
12
|
+
|
|
13
|
+
from mini_arcade_core.backend import Backend
|
|
14
|
+
from mini_arcade_core.engine.render.context import RenderContext
|
|
15
|
+
from mini_arcade_core.engine.render.effects.base import EffectParams
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
@dataclass
|
|
19
|
+
class VignetteNoiseEffect:
|
|
20
|
+
"""
|
|
21
|
+
Vignette + noise screen-space post effect.
|
|
22
|
+
Simulates a vignette effect with added noise/grain.
|
|
23
|
+
"""
|
|
24
|
+
|
|
25
|
+
effect_id: str = "vignette_noise"
|
|
26
|
+
|
|
27
|
+
# Justification: This is PoC code for v1.
|
|
28
|
+
# pylint: disable=too-many-locals
|
|
29
|
+
def apply(self, backend: Backend, ctx: RenderContext) -> None:
|
|
30
|
+
"""Apply the Vignette + Noise effect to the current render context."""
|
|
31
|
+
vp = ctx.viewport
|
|
32
|
+
x0, y0 = vp.offset_x, vp.offset_y
|
|
33
|
+
w, h = vp.viewport_w, vp.viewport_h
|
|
34
|
+
|
|
35
|
+
stack = ctx.meta.get("effects_stack")
|
|
36
|
+
params: EffectParams = (
|
|
37
|
+
stack.params.get(self.effect_id, EffectParams())
|
|
38
|
+
if stack
|
|
39
|
+
else EffectParams()
|
|
40
|
+
)
|
|
41
|
+
|
|
42
|
+
intensity = max(0.0, min(1.0, params.intensity))
|
|
43
|
+
if intensity <= 0.0:
|
|
44
|
+
return
|
|
45
|
+
|
|
46
|
+
backend.set_clip_rect(x0, y0, w, h)
|
|
47
|
+
|
|
48
|
+
# Vignette approximation: draw edge rectangles with increasing alpha.
|
|
49
|
+
# Not a true radial gradient, but good enough for v1.
|
|
50
|
+
steps = 10
|
|
51
|
+
max_alpha = int(110 * intensity) # subtle
|
|
52
|
+
for i in range(steps):
|
|
53
|
+
# thickness grows inward
|
|
54
|
+
t = i + 1
|
|
55
|
+
alpha = int(max_alpha * (t / steps))
|
|
56
|
+
color = (0, 0, 0, alpha)
|
|
57
|
+
|
|
58
|
+
# top
|
|
59
|
+
backend.draw_rect(x0, y0, w, t, color=color)
|
|
60
|
+
# bottom
|
|
61
|
+
backend.draw_rect(x0, y0 + h - t, w, t, color=color)
|
|
62
|
+
# left
|
|
63
|
+
backend.draw_rect(x0, y0, t, h, color=color)
|
|
64
|
+
# right
|
|
65
|
+
backend.draw_rect(x0 + w - t, y0, t, h, color=color)
|
|
66
|
+
|
|
67
|
+
# Noise: sprinkle a few pixels (or tiny 1x1 rects).
|
|
68
|
+
# Use deterministic-ish seed per frame so it doesn't “swim” too much.
|
|
69
|
+
frame = int(ctx.meta.get("frame_index", 0))
|
|
70
|
+
random.seed(frame * 1337)
|
|
71
|
+
|
|
72
|
+
dots = int(200 * intensity) # tweak
|
|
73
|
+
for _ in range(dots):
|
|
74
|
+
px = x0 + random.randint(0, max(0, w - 1))
|
|
75
|
+
py = y0 + random.randint(0, max(0, h - 1))
|
|
76
|
+
a = random.randint(10, int(50 * intensity) + 10)
|
|
77
|
+
backend.draw_rect(px, py, 1, 1, color=(255, 255, 255, a))
|
|
78
|
+
|
|
79
|
+
backend.clear_clip_rect()
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Frame packet for rendering.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
from __future__ import annotations
|
|
6
|
+
|
|
7
|
+
from dataclasses import dataclass
|
|
8
|
+
|
|
9
|
+
from mini_arcade_core.engine.render.packet import RenderPacket
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
@dataclass(frozen=True)
|
|
13
|
+
class FramePacket:
|
|
14
|
+
"""
|
|
15
|
+
A packet representing a frame to be rendered, associated with a specific scene
|
|
16
|
+
and indicating whether it is an overlay.
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
:ivar scene_id (str): Identifier of the scene.
|
|
20
|
+
:ivar is_overlay (bool): Whether the frame is an overlay.
|
|
21
|
+
:ivar packet (RenderPacket): The render packet containing rendering operations.
|
|
22
|
+
"""
|
|
23
|
+
|
|
24
|
+
scene_id: str
|
|
25
|
+
is_overlay: bool
|
|
26
|
+
packet: RenderPacket
|
|
File without changes
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Render pass base protocol.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
from __future__ import annotations
|
|
6
|
+
|
|
7
|
+
from typing import Protocol
|
|
8
|
+
|
|
9
|
+
from mini_arcade_core.backend import Backend
|
|
10
|
+
from mini_arcade_core.engine.render.context import RenderContext
|
|
11
|
+
from mini_arcade_core.engine.render.packet import RenderPacket
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class RenderPass(Protocol):
|
|
15
|
+
"""
|
|
16
|
+
Render pass protocol.
|
|
17
|
+
|
|
18
|
+
:ivar name: str: Name of the render pass.
|
|
19
|
+
"""
|
|
20
|
+
|
|
21
|
+
name: str
|
|
22
|
+
|
|
23
|
+
def run(
|
|
24
|
+
self, backend: Backend, ctx: RenderContext, packets: list[RenderPacket]
|
|
25
|
+
):
|
|
26
|
+
"""
|
|
27
|
+
Run the render pass.
|
|
28
|
+
|
|
29
|
+
:param backend: Backend: The rendering backend.
|
|
30
|
+
:type backend: Backend
|
|
31
|
+
|
|
32
|
+
:param ctx: RenderContext: The rendering context.
|
|
33
|
+
:type ctx: RenderContext
|
|
34
|
+
|
|
35
|
+
:param packets: list[RenderPacket]: List of render packets to process.
|
|
36
|
+
:type packets: list[RenderPacket]
|
|
37
|
+
"""
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Begin Frame Render Pass
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
from dataclasses import dataclass
|
|
6
|
+
|
|
7
|
+
from mini_arcade_core.backend import Backend
|
|
8
|
+
from mini_arcade_core.engine.render.context import RenderContext
|
|
9
|
+
from mini_arcade_core.engine.render.packet import RenderPacket
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
@dataclass
|
|
13
|
+
class BeginFramePass:
|
|
14
|
+
"""
|
|
15
|
+
Begin Frame Render Pass.
|
|
16
|
+
This pass signals the start of a new frame to the backend.
|
|
17
|
+
"""
|
|
18
|
+
|
|
19
|
+
name: str = "BeginFrame"
|
|
20
|
+
|
|
21
|
+
# Justification: some arguments are unused but required by the protocol
|
|
22
|
+
# pylint: disable=unused-argument
|
|
23
|
+
def run(
|
|
24
|
+
self, backend: Backend, ctx: RenderContext, packets: list[RenderPacket]
|
|
25
|
+
):
|
|
26
|
+
"""Run the begin frame pass."""
|
|
27
|
+
backend.begin_frame()
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
"""
|
|
2
|
+
End Frame render pass implementation.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
from dataclasses import dataclass
|
|
6
|
+
|
|
7
|
+
from mini_arcade_core.backend import Backend
|
|
8
|
+
from mini_arcade_core.engine.render.context import RenderContext
|
|
9
|
+
from mini_arcade_core.engine.render.packet import RenderPacket
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
@dataclass
|
|
13
|
+
class EndFramePass:
|
|
14
|
+
"""
|
|
15
|
+
End Frame Render Pass.
|
|
16
|
+
This pass signals the end of the current frame to the backend.
|
|
17
|
+
"""
|
|
18
|
+
|
|
19
|
+
name: str = "EndFrame"
|
|
20
|
+
|
|
21
|
+
# Justification: some arguments are unused but required by the protocol
|
|
22
|
+
# pylint: disable=unused-argument
|
|
23
|
+
def run(
|
|
24
|
+
self, backend: Backend, ctx: RenderContext, packets: list[RenderPacket]
|
|
25
|
+
):
|
|
26
|
+
"""Run the end frame pass."""
|
|
27
|
+
# Signal the end of the frame to the backend
|
|
28
|
+
backend.end_frame()
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Lighting render pass implementation.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
from dataclasses import dataclass
|
|
6
|
+
|
|
7
|
+
from mini_arcade_core.backend import Backend
|
|
8
|
+
from mini_arcade_core.engine.render.context import RenderContext
|
|
9
|
+
from mini_arcade_core.engine.render.packet import RenderPacket
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
@dataclass
|
|
13
|
+
class LightingPass:
|
|
14
|
+
"""
|
|
15
|
+
Lighting Render Pass.
|
|
16
|
+
This pass handles scene lighting effects.
|
|
17
|
+
"""
|
|
18
|
+
|
|
19
|
+
name: str = "LightingPass"
|
|
20
|
+
|
|
21
|
+
# Justification: No implementation yet
|
|
22
|
+
# pylint: disable=unused-argument
|
|
23
|
+
def run(
|
|
24
|
+
self, backend: Backend, ctx: RenderContext, packets: list[RenderPacket]
|
|
25
|
+
):
|
|
26
|
+
"""Run the lighting render pass."""
|
|
27
|
+
# hook/no-op for now
|
|
28
|
+
return
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Post-processing effects render pass implementation.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
from __future__ import annotations
|
|
6
|
+
|
|
7
|
+
from dataclasses import dataclass
|
|
8
|
+
|
|
9
|
+
from mini_arcade_core.backend import Backend
|
|
10
|
+
from mini_arcade_core.engine.render.context import RenderContext
|
|
11
|
+
from mini_arcade_core.engine.render.effects.registry import EffectRegistry
|
|
12
|
+
from mini_arcade_core.engine.render.frame_packet import FramePacket
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
@dataclass
|
|
16
|
+
class PostFXPass:
|
|
17
|
+
"""
|
|
18
|
+
PostFX Render Pass.
|
|
19
|
+
This pass handles post-processing effects like CRT simulation.
|
|
20
|
+
"""
|
|
21
|
+
|
|
22
|
+
name: str = "PostFXPass"
|
|
23
|
+
registry: EffectRegistry | None = None
|
|
24
|
+
|
|
25
|
+
# Justification: No implementation yet
|
|
26
|
+
# pylint: disable=unused-argument
|
|
27
|
+
def run(
|
|
28
|
+
self, backend: Backend, ctx: RenderContext, packets: list[FramePacket]
|
|
29
|
+
):
|
|
30
|
+
"""Run the post-processing effects render pass."""
|
|
31
|
+
# Zero overhead path (no effects configured)
|
|
32
|
+
stack = ctx.meta.get("effects_stack")
|
|
33
|
+
if stack is None or not stack.is_active():
|
|
34
|
+
return
|
|
35
|
+
|
|
36
|
+
# Screen space: no transforms
|
|
37
|
+
backend.clear_viewport_transform()
|
|
38
|
+
backend.clear_clip_rect()
|
|
39
|
+
|
|
40
|
+
reg = self.registry
|
|
41
|
+
if reg is None:
|
|
42
|
+
# no registry => nothing to do
|
|
43
|
+
return
|
|
44
|
+
|
|
45
|
+
for effect_id in list(stack.active):
|
|
46
|
+
effect = reg.get(effect_id)
|
|
47
|
+
if effect is None:
|
|
48
|
+
continue
|
|
49
|
+
effect.apply(backend, ctx)
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
"""
|
|
2
|
+
UI render pass implementation.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
from dataclasses import dataclass
|
|
6
|
+
|
|
7
|
+
from mini_arcade_core.backend import Backend
|
|
8
|
+
from mini_arcade_core.engine.render.context import RenderContext
|
|
9
|
+
from mini_arcade_core.engine.render.frame_packet import FramePacket
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
@dataclass
|
|
13
|
+
class UIPass:
|
|
14
|
+
"""
|
|
15
|
+
UI Render Pass.
|
|
16
|
+
This pass handles rendering of UI overlays.
|
|
17
|
+
"""
|
|
18
|
+
|
|
19
|
+
name: str = "UIPass"
|
|
20
|
+
|
|
21
|
+
def run(
|
|
22
|
+
self, backend: Backend, ctx: RenderContext, packets: list[FramePacket]
|
|
23
|
+
):
|
|
24
|
+
"""Run the UI render pass."""
|
|
25
|
+
# UI overlays should be screen-space (no world transform / no clip unless you want it)
|
|
26
|
+
backend.clear_viewport_transform()
|
|
27
|
+
backend.clear_clip_rect()
|
|
28
|
+
|
|
29
|
+
for fp in packets:
|
|
30
|
+
if not fp.is_overlay:
|
|
31
|
+
continue
|
|
32
|
+
if not fp.packet or not fp.packet.ops:
|
|
33
|
+
continue
|
|
34
|
+
|
|
35
|
+
# count overlays too (optional; I’d count them)
|
|
36
|
+
ctx.stats.packets += 1
|
|
37
|
+
ctx.stats.renderables += len(fp.packet.ops)
|
|
38
|
+
ctx.stats.draw_groups += 1
|
|
39
|
+
|
|
40
|
+
for op in fp.packet.ops:
|
|
41
|
+
op(backend)
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
"""
|
|
2
|
+
World render pass implementation.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
from dataclasses import dataclass
|
|
6
|
+
|
|
7
|
+
from mini_arcade_core.backend import Backend
|
|
8
|
+
from mini_arcade_core.engine.render.context import RenderContext
|
|
9
|
+
from mini_arcade_core.engine.render.frame_packet import FramePacket
|
|
10
|
+
from mini_arcade_core.engine.render.packet import RenderPacket
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
@dataclass
|
|
14
|
+
class WorldPass:
|
|
15
|
+
"""
|
|
16
|
+
World Render Pass.
|
|
17
|
+
This pass handles rendering of world-space objects.
|
|
18
|
+
"""
|
|
19
|
+
|
|
20
|
+
name: str = "WorldPass"
|
|
21
|
+
|
|
22
|
+
def run(
|
|
23
|
+
self, backend: Backend, ctx: RenderContext, packets: list[FramePacket]
|
|
24
|
+
):
|
|
25
|
+
"""Run the world render pass."""
|
|
26
|
+
for fp in packets:
|
|
27
|
+
if fp.is_overlay:
|
|
28
|
+
continue
|
|
29
|
+
self._draw_packet(backend, ctx, fp.packet)
|
|
30
|
+
|
|
31
|
+
def _draw_packet(
|
|
32
|
+
self, backend: Backend, ctx: RenderContext, packet: RenderPacket
|
|
33
|
+
):
|
|
34
|
+
if not packet or not packet.ops:
|
|
35
|
+
return
|
|
36
|
+
|
|
37
|
+
ctx.stats.packets += 1
|
|
38
|
+
ctx.stats.renderables += len(packet.ops)
|
|
39
|
+
ctx.stats.draw_groups += 1 # approx: 1 group per packet
|
|
40
|
+
|
|
41
|
+
backend.set_viewport_transform(
|
|
42
|
+
ctx.viewport.offset_x, ctx.viewport.offset_y, ctx.viewport.scale
|
|
43
|
+
)
|
|
44
|
+
backend.set_clip_rect(
|
|
45
|
+
ctx.viewport.offset_x,
|
|
46
|
+
ctx.viewport.offset_y,
|
|
47
|
+
ctx.viewport.viewport_w,
|
|
48
|
+
ctx.viewport.viewport_h,
|
|
49
|
+
)
|
|
50
|
+
try:
|
|
51
|
+
for op in packet.ops:
|
|
52
|
+
op(backend)
|
|
53
|
+
finally:
|
|
54
|
+
backend.clear_clip_rect()
|
|
55
|
+
backend.clear_viewport_transform()
|
|
@@ -3,12 +3,30 @@ Render pipeline module.
|
|
|
3
3
|
Defines the RenderPipeline class for rendering RenderPackets.
|
|
4
4
|
"""
|
|
5
5
|
|
|
6
|
+
# Justification: This code is duplicated in multiple places for clarity and separation
|
|
7
|
+
# of concerns.
|
|
8
|
+
# try:
|
|
9
|
+
# for op in packet.ops:
|
|
10
|
+
# op(backend)
|
|
11
|
+
# finally:
|
|
12
|
+
# backend.clear_clip_rect()
|
|
13
|
+
# backend.clear_viewport_transform() (duplicate-code)
|
|
14
|
+
# pylint: disable=duplicate-code
|
|
15
|
+
|
|
6
16
|
from __future__ import annotations
|
|
7
17
|
|
|
8
|
-
from dataclasses import dataclass
|
|
18
|
+
from dataclasses import dataclass, field
|
|
9
19
|
|
|
10
20
|
from mini_arcade_core.backend import Backend
|
|
21
|
+
from mini_arcade_core.engine.render.context import RenderContext
|
|
11
22
|
from mini_arcade_core.engine.render.packet import RenderPacket
|
|
23
|
+
from mini_arcade_core.engine.render.passes.base import RenderPass
|
|
24
|
+
from mini_arcade_core.engine.render.passes.begin_frame import BeginFramePass
|
|
25
|
+
from mini_arcade_core.engine.render.passes.end_frame import EndFramePass
|
|
26
|
+
from mini_arcade_core.engine.render.passes.lighting import LightingPass
|
|
27
|
+
from mini_arcade_core.engine.render.passes.postfx import PostFXPass
|
|
28
|
+
from mini_arcade_core.engine.render.passes.ui import UIPass
|
|
29
|
+
from mini_arcade_core.engine.render.passes.world import WorldPass
|
|
12
30
|
from mini_arcade_core.engine.render.viewport import ViewportState
|
|
13
31
|
|
|
14
32
|
|
|
@@ -18,12 +36,43 @@ class RenderPipeline:
|
|
|
18
36
|
Minimal pipeline for v1.
|
|
19
37
|
|
|
20
38
|
Later you can expand this into passes:
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
39
|
+
- build draw list
|
|
40
|
+
- cull
|
|
41
|
+
- sort
|
|
42
|
+
- backend draw pass
|
|
43
|
+
|
|
44
|
+
:cvar passes: list[RenderPass]: List of render passes to execute in order.
|
|
25
45
|
"""
|
|
26
46
|
|
|
47
|
+
passes: list[RenderPass] = field(
|
|
48
|
+
default_factory=lambda: [
|
|
49
|
+
BeginFramePass(),
|
|
50
|
+
WorldPass(),
|
|
51
|
+
LightingPass(),
|
|
52
|
+
UIPass(),
|
|
53
|
+
PostFXPass(),
|
|
54
|
+
EndFramePass(),
|
|
55
|
+
]
|
|
56
|
+
)
|
|
57
|
+
|
|
58
|
+
def render_frame(
|
|
59
|
+
self, backend: Backend, ctx: RenderContext, packets: list[RenderPacket]
|
|
60
|
+
):
|
|
61
|
+
"""
|
|
62
|
+
Render a frame using the provided Backend, RenderContext, and list of RenderPackets.
|
|
63
|
+
|
|
64
|
+
:param backend: Backend to use for rendering.
|
|
65
|
+
:type backend: Backend
|
|
66
|
+
|
|
67
|
+
:param ctx: RenderContext containing rendering state.
|
|
68
|
+
:type ctx: RenderContext
|
|
69
|
+
|
|
70
|
+
:param packets: List of RenderPackets to render.
|
|
71
|
+
:type packets: list[RenderPacket]
|
|
72
|
+
"""
|
|
73
|
+
for p in self.passes:
|
|
74
|
+
p.run(backend, ctx, packets)
|
|
75
|
+
|
|
27
76
|
def draw_packet(
|
|
28
77
|
self,
|
|
29
78
|
backend: Backend,
|
|
@@ -48,12 +97,12 @@ class RenderPipeline:
|
|
|
48
97
|
viewport_state.scale,
|
|
49
98
|
)
|
|
50
99
|
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
100
|
+
backend.set_clip_rect(
|
|
101
|
+
viewport_state.offset_x,
|
|
102
|
+
viewport_state.offset_y,
|
|
103
|
+
viewport_state.viewport_w,
|
|
104
|
+
viewport_state.viewport_h,
|
|
105
|
+
)
|
|
57
106
|
|
|
58
107
|
try:
|
|
59
108
|
for op in packet.ops:
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Render service definition.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
from __future__ import annotations
|
|
6
|
+
|
|
7
|
+
from dataclasses import dataclass
|
|
8
|
+
|
|
9
|
+
from mini_arcade_core.engine.render.context import RenderStats
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
@dataclass
|
|
13
|
+
class RenderService:
|
|
14
|
+
"""
|
|
15
|
+
Render Service.
|
|
16
|
+
This service manages rendering statistics and state.
|
|
17
|
+
:ivar last_frame_ms (float): Time taken for the last frame in milliseconds.
|
|
18
|
+
:ivar last_stats (RenderStats): Rendering statistics from the last frame.
|
|
19
|
+
"""
|
|
20
|
+
|
|
21
|
+
last_frame_ms: float = 0.0
|
|
22
|
+
last_stats: RenderStats = RenderStats()
|
|
File without changes
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Render service definition.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
from __future__ import annotations
|
|
6
|
+
|
|
7
|
+
from typing import Protocol
|
|
8
|
+
|
|
9
|
+
from mini_arcade_core.engine.render.context import RenderStats
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class RenderServicePort(Protocol):
|
|
13
|
+
"""
|
|
14
|
+
Render Service.
|
|
15
|
+
This service manages rendering statistics and state.
|
|
16
|
+
|
|
17
|
+
:ivar last_frame_ms (float): Time taken for the last frame in milliseconds.
|
|
18
|
+
:ivar last_stats (RenderStats): Rendering statistics from the last frame.
|
|
19
|
+
"""
|
|
20
|
+
|
|
21
|
+
last_frame_ms: float
|
|
22
|
+
last_stats: RenderStats
|
|
@@ -116,7 +116,7 @@ class SceneAdapter(ScenePort):
|
|
|
116
116
|
def has_scene(self, scene_id: str) -> bool:
|
|
117
117
|
return any(item.entry.scene_id == scene_id for item in self._stack)
|
|
118
118
|
|
|
119
|
-
def remove_scene(self, scene_id: str)
|
|
119
|
+
def remove_scene(self, scene_id: str):
|
|
120
120
|
# remove first match from top (overlay is usually near top)
|
|
121
121
|
for i in range(len(self._stack) - 1, -1, -1):
|
|
122
122
|
if self._stack[i].entry.scene_id == scene_id:
|
|
@@ -10,6 +10,7 @@ from mini_arcade_core.runtime.audio.audio_port import AudioPort
|
|
|
10
10
|
from mini_arcade_core.runtime.capture.capture_port import CapturePort
|
|
11
11
|
from mini_arcade_core.runtime.file.file_port import FilePort
|
|
12
12
|
from mini_arcade_core.runtime.input.input_port import InputPort
|
|
13
|
+
from mini_arcade_core.runtime.render.render_port import RenderServicePort
|
|
13
14
|
from mini_arcade_core.runtime.scene.scene_port import ScenePort
|
|
14
15
|
from mini_arcade_core.runtime.window.window_port import WindowPort
|
|
15
16
|
|
|
@@ -33,3 +34,4 @@ class RuntimeServices:
|
|
|
33
34
|
files: FilePort
|
|
34
35
|
capture: CapturePort
|
|
35
36
|
input: InputPort
|
|
37
|
+
render: RenderServicePort
|
|
@@ -39,10 +39,13 @@ class DebugOverlayScene(SimScene):
|
|
|
39
39
|
vp = services.window.get_viewport()
|
|
40
40
|
stack = services.scenes.visible_entries()
|
|
41
41
|
# pylint: enable=assignment-from-no-return
|
|
42
|
-
|
|
42
|
+
rs = services.render
|
|
43
43
|
lines = [
|
|
44
44
|
f"FPS: {self._fps:5.1f}",
|
|
45
45
|
f"dt: {dt*1000.0:5.2f} ms",
|
|
46
|
+
f"frame: {rs.last_frame_ms:5.2f} ms",
|
|
47
|
+
f"renderables: {rs.last_stats.renderables}",
|
|
48
|
+
f"draw_groups~: {rs.last_stats.draw_groups}",
|
|
46
49
|
f"virtual: {vp.virtual_w}x{vp.virtual_h}",
|
|
47
50
|
f"window: {vp.window_w}x{vp.window_h}",
|
|
48
51
|
f"scale: {vp.scale:.3f}",
|
|
@@ -15,8 +15,7 @@ from mini_arcade_core.runtime.context import RuntimeContext
|
|
|
15
15
|
from .autoreg import snapshot
|
|
16
16
|
|
|
17
17
|
if TYPE_CHECKING:
|
|
18
|
-
from mini_arcade_core.
|
|
19
|
-
from mini_arcade_core.sim import SimScene
|
|
18
|
+
from mini_arcade_core.sim.protocols import SimScene
|
|
20
19
|
|
|
21
20
|
|
|
22
21
|
class SceneFactory(Protocol):
|
|
@@ -1,17 +1,33 @@
|
|
|
1
1
|
mini_arcade_core/__init__.py,sha256=axwl7fiQ2Zu2vPOTMUxwnvR746gI9RhpBWpBCId_yqo,3686
|
|
2
2
|
mini_arcade_core/backend/__init__.py,sha256=E9uOCttCkXwdN_5MlcFHUmG3Bj6RYMatNNOno4C_6aI,312
|
|
3
|
-
mini_arcade_core/backend/backend.py,sha256=
|
|
3
|
+
mini_arcade_core/backend/backend.py,sha256=1-lgjkcftYaeusWKhQrDTwgfbwH6y9S-PP-miy7tRNE,8619
|
|
4
4
|
mini_arcade_core/backend/events.py,sha256=5Ohve3CQ6n2CztiOhbCoz6yFDY4z0j4v4R9FBKRDRjc,2929
|
|
5
5
|
mini_arcade_core/backend/keys.py,sha256=LTg20SwLBI3kpPIiTNpq2yBft_QUGj-iNFSNm9M-Fus,3010
|
|
6
6
|
mini_arcade_core/backend/sdl_map.py,sha256=_yBRtvaFUcQKy1kcoIf-SPhbbKEW7dzvzBcI6TLmKjc,2060
|
|
7
7
|
mini_arcade_core/backend/types.py,sha256=SuiwXGNmXCZxfPsww6zj3V_NK7k4jpoCuzMn19afS-g,175
|
|
8
8
|
mini_arcade_core/bus.py,sha256=2Etpoa-UWhk33xJjqDlY5YslPDJEjxNoIEVtF3C73vs,1558
|
|
9
9
|
mini_arcade_core/engine/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
10
|
-
mini_arcade_core/engine/commands.py,sha256=
|
|
11
|
-
mini_arcade_core/engine/game.py,sha256=
|
|
10
|
+
mini_arcade_core/engine/commands.py,sha256=SfhncRQvuwTQg5BEyS8yeFTaq5mixiFsvFUZSBK9y1Q,5420
|
|
11
|
+
mini_arcade_core/engine/game.py,sha256=HaLCew31UD8ujz3yOHGjWPzjpD7dkeQSMDdnMFD9JtI,15588
|
|
12
12
|
mini_arcade_core/engine/render/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
13
|
+
mini_arcade_core/engine/render/context.py,sha256=1zzycXDbQnK3wuk79cMjSewu8FinAp-EsFal7bbJFnI,1414
|
|
14
|
+
mini_arcade_core/engine/render/effects/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
15
|
+
mini_arcade_core/engine/render/effects/base.py,sha256=yq8qRZMbp4VTi02WnVbyHS3RYc56vvP69MzPbCO3m2s,2373
|
|
16
|
+
mini_arcade_core/engine/render/effects/crt.py,sha256=D8_7Bqzt_VeLusyXzeLQynxTIZwQ45NisSImTB-4Hpg,2141
|
|
17
|
+
mini_arcade_core/engine/render/effects/registry.py,sha256=ie1nuLJbQq9Ecb9tea3oGvSD9QaydbFFhH8ggEf-9uU,1220
|
|
18
|
+
mini_arcade_core/engine/render/effects/vignette.py,sha256=ogLTgaJ9srpiA59Lc7_nBD7wZk7y6t9HnHYNoK7YeSE,2569
|
|
19
|
+
mini_arcade_core/engine/render/frame_packet.py,sha256=nYHvR7CHlIZa6ZazmPO2dU2P91vEkBjBzUVQGrOkaYc,624
|
|
13
20
|
mini_arcade_core/engine/render/packet.py,sha256=OiAPwGoVHo04OcUWMAoA_N1AFPUMyf8yxNgJthGj4-c,1440
|
|
14
|
-
mini_arcade_core/engine/render/
|
|
21
|
+
mini_arcade_core/engine/render/passes/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
22
|
+
mini_arcade_core/engine/render/passes/base.py,sha256=LWgWhfafbCvRKIFbt3koW-ibjYxMfKOIXyLNazCakcM,864
|
|
23
|
+
mini_arcade_core/engine/render/passes/begin_frame.py,sha256=Yzlk8bv7mgjlnM1jH_kYjJ_d8469i8aPriEqv7hm9gE,699
|
|
24
|
+
mini_arcade_core/engine/render/passes/end_frame.py,sha256=ONFcNQJhYvaN-BK6wyxxsLDBUUHx-b-mvOYH__QXJJ4,760
|
|
25
|
+
mini_arcade_core/engine/render/passes/lighting.py,sha256=ugmHHNNZWArL_Xs6-1SQIxLptUTQwe4M2sSPEk8X-7s,677
|
|
26
|
+
mini_arcade_core/engine/render/passes/postfx.py,sha256=OWcayjffZr5xT7cNIsbm4gzxmGfYiQyLgycV2dC6Jsg,1417
|
|
27
|
+
mini_arcade_core/engine/render/passes/ui.py,sha256=JxMXI900le45nOZF2SzDuniVqUOJYvAbSDRxYcIFFkc,1122
|
|
28
|
+
mini_arcade_core/engine/render/passes/world.py,sha256=QPKDVkKk-AA7ZR1HCo-JfzjxjUT6vmxXbqo1BYpebiU,1550
|
|
29
|
+
mini_arcade_core/engine/render/pipeline.py,sha256=bp2pOuwwBvJjYWZJgNe6xTfEBwdEm5WiBnM8infV_AI,3268
|
|
30
|
+
mini_arcade_core/engine/render/render_service.py,sha256=1ueir8MZ6Six5gAHt5StoICPAbyppX4DqzWb8HEuS9g,531
|
|
15
31
|
mini_arcade_core/engine/render/viewport.py,sha256=fbzH3_rc27IGUtDalmxz5cukwHnpt1kopIeVqmTab20,5784
|
|
16
32
|
mini_arcade_core/managers/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
17
33
|
mini_arcade_core/managers/cheats.py,sha256=jMx2a8YnaNCkCG5MPmIzz4uHuS7-_aYf0J45cv2-3v0,5569
|
|
@@ -31,17 +47,19 @@ mini_arcade_core/runtime/input/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5
|
|
|
31
47
|
mini_arcade_core/runtime/input/input_adapter.py,sha256=vExQiwFIWTI3zYD8lmnD9TvoQPZvJfI6IINPJUqAdQ0,1467
|
|
32
48
|
mini_arcade_core/runtime/input/input_port.py,sha256=d4ptftwf92_LJdyaUMFxIsLHXBINzQyJACHn4laNyxQ,746
|
|
33
49
|
mini_arcade_core/runtime/input_frame.py,sha256=34-RAfOD-YScVLyRQrarpm7byFTHjsWM77lIH0JsmT8,2384
|
|
50
|
+
mini_arcade_core/runtime/render/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
51
|
+
mini_arcade_core/runtime/render/render_port.py,sha256=Sqp-JBh-iRzzGtgnO_nU1KiJEqyrTYPRDQbg04HdR0A,507
|
|
34
52
|
mini_arcade_core/runtime/scene/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
35
|
-
mini_arcade_core/runtime/scene/scene_adapter.py,sha256=
|
|
36
|
-
mini_arcade_core/runtime/scene/scene_port.py,sha256=
|
|
37
|
-
mini_arcade_core/runtime/services.py,sha256=
|
|
53
|
+
mini_arcade_core/runtime/scene/scene_adapter.py,sha256=RthYyp_G3c9YVVQlnCuvtgFFtp9zvdjcykc0fXEpmic,3473
|
|
54
|
+
mini_arcade_core/runtime/scene/scene_port.py,sha256=t6-REznEVvxcJoq2DJXcDs8sg0nC9fww6J5NwGpweg8,4473
|
|
55
|
+
mini_arcade_core/runtime/services.py,sha256=dtatQisOAdWgdqGjrH0cO1GA6ZDt4CiXjOEzlaNL51E,1165
|
|
38
56
|
mini_arcade_core/runtime/window/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
39
57
|
mini_arcade_core/runtime/window/window_adapter.py,sha256=_yCcJwvHN0gEPh0rkgLEKtPZ50qRbONsUTbgtV1m7Y8,2689
|
|
40
58
|
mini_arcade_core/runtime/window/window_port.py,sha256=JbSH549De7fa4ifQ0EH5QQoq03Got1n9C4qViLgciUU,2682
|
|
41
59
|
mini_arcade_core/scenes/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
42
60
|
mini_arcade_core/scenes/autoreg.py,sha256=wsuY7YUSZFmDyToKHFriAG78OU48-7J4BfL_X6T5GBg,1037
|
|
43
|
-
mini_arcade_core/scenes/debug_overlay.py,sha256=
|
|
44
|
-
mini_arcade_core/scenes/registry.py,sha256=
|
|
61
|
+
mini_arcade_core/scenes/debug_overlay.py,sha256=pe5eXmxVl0itBdlt5Q-TEyGNiONMztMeck6Vq93QCmo,2423
|
|
62
|
+
mini_arcade_core/scenes/registry.py,sha256=SKjaw1e2EHvLimxe4ixf0DFxQTJRVQxRi96MujGgCao,3376
|
|
45
63
|
mini_arcade_core/scenes/sim_scene.py,sha256=b2JsOvPFkHCdCf8pMLJZ90qB0JJ6B8Ka3o5QK4cVshI,1055
|
|
46
64
|
mini_arcade_core/scenes/systems/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
47
65
|
mini_arcade_core/scenes/systems/base_system.py,sha256=GfMrXsO8ynW3xOxWeav7Ug5XUbRnbF0vo8VzmG7gpec,1075
|
|
@@ -61,7 +79,7 @@ mini_arcade_core/ui/menu.py,sha256=pUC6qfG3lNXa1Ga2Kz_cSgjiAfAWVMRevum61G_RE8k,2
|
|
|
61
79
|
mini_arcade_core/utils/__init__.py,sha256=3Q9r6bTyqImYix8BnOGwWjAz25nbTQezGcRq3m5KEYE,189
|
|
62
80
|
mini_arcade_core/utils/deprecated_decorator.py,sha256=yrrW2ZqPskK-4MUTyIrMb465Wc54X2poV53ZQutZWqc,1140
|
|
63
81
|
mini_arcade_core/utils/logging.py,sha256=YyirsGRSpGtxegUl3HWz37mGNngK3QkYm2_aZjXJC84,5279
|
|
64
|
-
mini_arcade_core-1.
|
|
65
|
-
mini_arcade_core-1.
|
|
66
|
-
mini_arcade_core-1.
|
|
67
|
-
mini_arcade_core-1.
|
|
82
|
+
mini_arcade_core-1.1.1.dist-info/METADATA,sha256=uLI8iX6Z3XyhoHBQDw4vmWfXRkk-09IXqUvHMvwHrJQ,8188
|
|
83
|
+
mini_arcade_core-1.1.1.dist-info/WHEEL,sha256=3ny-bZhpXrU6vSQ1UPG34FoxZBp3lVcvK0LkgUz6VLk,88
|
|
84
|
+
mini_arcade_core-1.1.1.dist-info/licenses/LICENSE,sha256=3lHAuV0584cVS5vAqi2uC6GcsVgxUijvwvtZckyvaZ4,1096
|
|
85
|
+
mini_arcade_core-1.1.1.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|