mini-arcade-core 1.1.1__py3-none-any.whl → 1.2.0__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/__init__.py +14 -42
- mini_arcade_core/backend/__init__.py +1 -2
- mini_arcade_core/backend/backend.py +182 -184
- mini_arcade_core/backend/types.py +5 -1
- mini_arcade_core/engine/commands.py +8 -8
- mini_arcade_core/engine/game.py +54 -354
- mini_arcade_core/engine/game_config.py +40 -0
- mini_arcade_core/engine/gameplay_settings.py +24 -0
- mini_arcade_core/engine/loop/config.py +20 -0
- mini_arcade_core/engine/loop/hooks.py +77 -0
- mini_arcade_core/engine/loop/runner.py +272 -0
- mini_arcade_core/engine/loop/state.py +32 -0
- mini_arcade_core/engine/managers.py +24 -0
- mini_arcade_core/engine/render/context.py +0 -2
- mini_arcade_core/engine/render/effects/base.py +2 -2
- mini_arcade_core/engine/render/effects/crt.py +4 -4
- mini_arcade_core/engine/render/effects/registry.py +1 -1
- mini_arcade_core/engine/render/effects/vignette.py +8 -8
- mini_arcade_core/engine/render/passes/begin_frame.py +1 -1
- mini_arcade_core/engine/render/passes/end_frame.py +1 -1
- mini_arcade_core/engine/render/passes/postfx.py +1 -1
- mini_arcade_core/engine/render/passes/ui.py +1 -1
- mini_arcade_core/engine/render/passes/world.py +6 -6
- mini_arcade_core/engine/render/pipeline.py +7 -6
- mini_arcade_core/engine/render/viewport.py +10 -4
- mini_arcade_core/engine/scenes/models.py +54 -0
- mini_arcade_core/engine/scenes/scene_manager.py +213 -0
- mini_arcade_core/runtime/audio/audio_adapter.py +4 -3
- mini_arcade_core/runtime/audio/audio_port.py +0 -4
- mini_arcade_core/runtime/capture/capture_adapter.py +13 -6
- mini_arcade_core/runtime/capture/capture_port.py +0 -4
- mini_arcade_core/runtime/context.py +8 -6
- mini_arcade_core/runtime/scene/scene_query_adapter.py +31 -0
- mini_arcade_core/runtime/scene/scene_query_port.py +38 -0
- mini_arcade_core/runtime/services.py +3 -2
- mini_arcade_core/runtime/window/window_adapter.py +43 -41
- mini_arcade_core/runtime/window/window_port.py +3 -17
- mini_arcade_core/scenes/debug_overlay.py +5 -4
- mini_arcade_core/scenes/registry.py +11 -1
- mini_arcade_core/scenes/sim_scene.py +14 -14
- mini_arcade_core/ui/menu.py +54 -16
- mini_arcade_core/utils/__init__.py +2 -1
- mini_arcade_core/utils/logging.py +47 -18
- mini_arcade_core/utils/profiler.py +283 -0
- {mini_arcade_core-1.1.1.dist-info → mini_arcade_core-1.2.0.dist-info}/METADATA +1 -1
- mini_arcade_core-1.2.0.dist-info/RECORD +92 -0
- {mini_arcade_core-1.1.1.dist-info → mini_arcade_core-1.2.0.dist-info}/WHEEL +1 -1
- mini_arcade_core/managers/inputs.py +0 -284
- mini_arcade_core/runtime/scene/scene_adapter.py +0 -125
- mini_arcade_core/runtime/scene/scene_port.py +0 -170
- mini_arcade_core/sim/protocols.py +0 -41
- mini_arcade_core/sim/runner.py +0 -222
- mini_arcade_core-1.1.1.dist-info/RECORD +0 -85
- /mini_arcade_core/{managers → engine}/cheats.py +0 -0
- /mini_arcade_core/{managers → engine/loop}/__init__.py +0 -0
- /mini_arcade_core/{sim → engine/scenes}/__init__.py +0 -0
- {mini_arcade_core-1.1.1.dist-info → mini_arcade_core-1.2.0.dist-info}/licenses/LICENSE +0 -0
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Game core module defining the Game class and configuration.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
from __future__ import annotations
|
|
6
|
+
|
|
7
|
+
from typing import TYPE_CHECKING, Iterable, Protocol
|
|
8
|
+
|
|
9
|
+
from mini_arcade_core.backend.events import Event, EventType
|
|
10
|
+
from mini_arcade_core.backend.keys import Key
|
|
11
|
+
from mini_arcade_core.engine.commands import (
|
|
12
|
+
ToggleDebugOverlayCommand,
|
|
13
|
+
ToggleEffectCommand,
|
|
14
|
+
)
|
|
15
|
+
from mini_arcade_core.engine.render.effects.base import EffectStack
|
|
16
|
+
from mini_arcade_core.utils import logger
|
|
17
|
+
|
|
18
|
+
if TYPE_CHECKING:
|
|
19
|
+
from mini_arcade_core.engine.game import Game
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class LoopHooks(Protocol):
|
|
23
|
+
"""
|
|
24
|
+
Protocol for custom loop hooks to handle events.
|
|
25
|
+
"""
|
|
26
|
+
|
|
27
|
+
def on_events(self, events: Iterable[object]):
|
|
28
|
+
"""
|
|
29
|
+
Docstring for on_events
|
|
30
|
+
|
|
31
|
+
:param events: Iterable of input events.
|
|
32
|
+
:type events: Iterable[object]
|
|
33
|
+
"""
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
class DefaultGameHooks:
|
|
37
|
+
"""
|
|
38
|
+
Default implementation of LoopHooks for handling common events.
|
|
39
|
+
|
|
40
|
+
:param game: The Game instance.
|
|
41
|
+
:type game: Game
|
|
42
|
+
:param effects_stack: The EffectStack for post-processing effects.
|
|
43
|
+
:type effects_stack: EffectStack
|
|
44
|
+
"""
|
|
45
|
+
|
|
46
|
+
def __init__(self, game: "Game", effects_stack: EffectStack):
|
|
47
|
+
self.game = game
|
|
48
|
+
self.effects_stack = effects_stack
|
|
49
|
+
|
|
50
|
+
def on_events(self, events: Iterable[Event]):
|
|
51
|
+
"""
|
|
52
|
+
Handle common events such as window resize and debug toggles.
|
|
53
|
+
|
|
54
|
+
:param events: Iterable of input events.
|
|
55
|
+
:type events: Iterable[Event]
|
|
56
|
+
"""
|
|
57
|
+
for e in events:
|
|
58
|
+
if e.type == EventType.WINDOWRESIZED and e.size:
|
|
59
|
+
w, h = e.size
|
|
60
|
+
logger.debug(f"Window resized event: {w}x{h}")
|
|
61
|
+
self.game.services.window.on_window_resized(w, h)
|
|
62
|
+
|
|
63
|
+
if e.type == EventType.KEYDOWN:
|
|
64
|
+
if e.key == Key.F1:
|
|
65
|
+
self.game.managers.command_queue.push(
|
|
66
|
+
ToggleDebugOverlayCommand()
|
|
67
|
+
)
|
|
68
|
+
elif e.key == Key.F2:
|
|
69
|
+
self.game.managers.command_queue.push(
|
|
70
|
+
ToggleEffectCommand("crt")
|
|
71
|
+
)
|
|
72
|
+
elif e.key == Key.F3:
|
|
73
|
+
self.game.managers.command_queue.push(
|
|
74
|
+
ToggleEffectCommand("vignette_noise")
|
|
75
|
+
)
|
|
76
|
+
elif e.key == Key.F4:
|
|
77
|
+
self.effects_stack.enabled = not self.effects_stack.enabled
|
|
@@ -0,0 +1,272 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Game core module defining the Game class and configuration.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
from __future__ import annotations
|
|
6
|
+
|
|
7
|
+
from time import sleep
|
|
8
|
+
from typing import TYPE_CHECKING
|
|
9
|
+
|
|
10
|
+
from mini_arcade_core.engine.commands import CommandContext, QuitCommand
|
|
11
|
+
from mini_arcade_core.engine.loop.config import RunnerConfig
|
|
12
|
+
from mini_arcade_core.engine.loop.hooks import LoopHooks
|
|
13
|
+
from mini_arcade_core.engine.loop.state import FrameState
|
|
14
|
+
from mini_arcade_core.engine.render.context import RenderContext
|
|
15
|
+
from mini_arcade_core.engine.render.effects.base import EffectStack
|
|
16
|
+
from mini_arcade_core.engine.render.frame_packet import FramePacket
|
|
17
|
+
from mini_arcade_core.engine.render.packet import RenderPacket
|
|
18
|
+
from mini_arcade_core.engine.render.pipeline import RenderPipeline
|
|
19
|
+
from mini_arcade_core.runtime.input_frame import InputFrame
|
|
20
|
+
from mini_arcade_core.utils import FrameTimer
|
|
21
|
+
|
|
22
|
+
if TYPE_CHECKING:
|
|
23
|
+
from mini_arcade_core.engine.game import Game
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def _neutral_input(frame_index: int, dt: float) -> InputFrame:
|
|
27
|
+
"""Create a neutral InputFrame with no input events."""
|
|
28
|
+
return InputFrame(frame_index=frame_index, dt=dt)
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
# Justification: This class has many attributes for managing the loop.
|
|
32
|
+
# pylint: disable=too-many-instance-attributes
|
|
33
|
+
class EngineRunner:
|
|
34
|
+
"""
|
|
35
|
+
Core engine runner responsible for the main loop execution.
|
|
36
|
+
|
|
37
|
+
:param game: The Game instance to run.
|
|
38
|
+
:type game: Game
|
|
39
|
+
:param pipeline: The RenderPipeline to use for rendering.
|
|
40
|
+
:type pipeline: RenderPipeline
|
|
41
|
+
:param effects_stack: The EffectStack for post-processing effects.
|
|
42
|
+
:type effects_stack: EffectStack
|
|
43
|
+
:param hooks: Optional LoopHooks for custom event handling.
|
|
44
|
+
:type hooks: LoopHooks | None
|
|
45
|
+
"""
|
|
46
|
+
|
|
47
|
+
def __init__(
|
|
48
|
+
self,
|
|
49
|
+
game: "Game",
|
|
50
|
+
*,
|
|
51
|
+
pipeline: RenderPipeline,
|
|
52
|
+
effects_stack: EffectStack,
|
|
53
|
+
hooks: LoopHooks | None = None,
|
|
54
|
+
):
|
|
55
|
+
self.game = game
|
|
56
|
+
self.backend = game.backend
|
|
57
|
+
self.services = game.services
|
|
58
|
+
self.managers = game.managers
|
|
59
|
+
|
|
60
|
+
self.pipeline = pipeline
|
|
61
|
+
self.effects_stack = effects_stack
|
|
62
|
+
self.hooks = hooks
|
|
63
|
+
|
|
64
|
+
self._running = False
|
|
65
|
+
self._packet_cache: dict[int, RenderPacket] = {}
|
|
66
|
+
|
|
67
|
+
def stop(self):
|
|
68
|
+
"""Stop the engine runner loop."""
|
|
69
|
+
self._running = False
|
|
70
|
+
|
|
71
|
+
def run(self, *, cfg: RunnerConfig, timer: FrameTimer | None = None):
|
|
72
|
+
"""
|
|
73
|
+
Run the main loop with the given configuration.
|
|
74
|
+
|
|
75
|
+
:param cfg: RunnerConfig instance.
|
|
76
|
+
:type cfg: RunnerConfig
|
|
77
|
+
:param timer: Optional FrameTimer for profiling.
|
|
78
|
+
:type timer: FrameTimer | None
|
|
79
|
+
"""
|
|
80
|
+
self._running = True
|
|
81
|
+
frame = FrameState()
|
|
82
|
+
|
|
83
|
+
target_dt = 1.0 / cfg.fps if cfg.fps > 0 else 0.0
|
|
84
|
+
|
|
85
|
+
while self._running and self.game.running:
|
|
86
|
+
if (
|
|
87
|
+
cfg.max_frames is not None
|
|
88
|
+
and frame.frame_index >= cfg.max_frames
|
|
89
|
+
):
|
|
90
|
+
break
|
|
91
|
+
|
|
92
|
+
if timer:
|
|
93
|
+
timer.clear()
|
|
94
|
+
timer.mark("frame_start")
|
|
95
|
+
|
|
96
|
+
frame.step_time()
|
|
97
|
+
|
|
98
|
+
events = self._poll_events(timer)
|
|
99
|
+
self._handle_events(events)
|
|
100
|
+
|
|
101
|
+
input_frame = self._build_input(events, frame=frame, timer=timer)
|
|
102
|
+
if self._should_quit(input_frame):
|
|
103
|
+
break
|
|
104
|
+
|
|
105
|
+
input_entry = self._input_entry()
|
|
106
|
+
if input_entry is None:
|
|
107
|
+
break
|
|
108
|
+
|
|
109
|
+
self._tick_scenes(
|
|
110
|
+
input_entry, input_frame, frame=frame, timer=timer
|
|
111
|
+
)
|
|
112
|
+
ctx = self._build_command_context(timer)
|
|
113
|
+
self._process_cheats(input_frame, ctx, timer)
|
|
114
|
+
self._execute_commands(ctx, timer)
|
|
115
|
+
|
|
116
|
+
self._render_frame(frame, timer)
|
|
117
|
+
|
|
118
|
+
self._sleep(target_dt, frame.dt, timer)
|
|
119
|
+
|
|
120
|
+
if timer and timer.should_report(frame.frame_index):
|
|
121
|
+
timer.emit(frame.frame_index)
|
|
122
|
+
|
|
123
|
+
frame.frame_index += 1
|
|
124
|
+
|
|
125
|
+
self.managers.scenes.clean()
|
|
126
|
+
|
|
127
|
+
def _poll_events(self, timer: FrameTimer | None):
|
|
128
|
+
# Poll input events from the backend.
|
|
129
|
+
events = list(self.backend.input.poll())
|
|
130
|
+
if timer:
|
|
131
|
+
timer.mark("events_polled")
|
|
132
|
+
return events
|
|
133
|
+
|
|
134
|
+
def _handle_events(self, events):
|
|
135
|
+
# Handle polled events via hooks if available.
|
|
136
|
+
if self.hooks:
|
|
137
|
+
self.hooks.on_events(events)
|
|
138
|
+
|
|
139
|
+
def _build_input(
|
|
140
|
+
self, events, *, frame: FrameState, timer: FrameTimer | None
|
|
141
|
+
):
|
|
142
|
+
# Build InputFrame from events.
|
|
143
|
+
input_frame = self.services.input.build(
|
|
144
|
+
events, frame.frame_index, frame.dt
|
|
145
|
+
)
|
|
146
|
+
if timer:
|
|
147
|
+
timer.mark("input_built")
|
|
148
|
+
if input_frame.quit:
|
|
149
|
+
self.managers.command_queue.push(QuitCommand())
|
|
150
|
+
return input_frame
|
|
151
|
+
|
|
152
|
+
def _should_quit(self, input_frame: InputFrame) -> bool:
|
|
153
|
+
# Determine if the game should quit based on input.
|
|
154
|
+
return bool(input_frame.quit)
|
|
155
|
+
|
|
156
|
+
def _input_entry(self):
|
|
157
|
+
# Get the current input-focused scene entry.
|
|
158
|
+
return self.managers.scenes.input_entry()
|
|
159
|
+
|
|
160
|
+
def _tick_scenes(
|
|
161
|
+
self,
|
|
162
|
+
input_entry,
|
|
163
|
+
input_frame: InputFrame,
|
|
164
|
+
*,
|
|
165
|
+
frame: FrameState,
|
|
166
|
+
timer: FrameTimer | None,
|
|
167
|
+
):
|
|
168
|
+
# Tick/update all scenes according to their policies.
|
|
169
|
+
if timer:
|
|
170
|
+
timer.mark("tick_start")
|
|
171
|
+
for entry in self.managers.scenes.update_entries():
|
|
172
|
+
effective_input = (
|
|
173
|
+
input_frame
|
|
174
|
+
if entry is input_entry
|
|
175
|
+
else _neutral_input(frame.frame_index, frame.dt)
|
|
176
|
+
)
|
|
177
|
+
packet = entry.scene.tick(effective_input, frame.dt)
|
|
178
|
+
self._packet_cache[id(entry.scene)] = packet
|
|
179
|
+
if timer:
|
|
180
|
+
timer.mark("tick_end")
|
|
181
|
+
|
|
182
|
+
def _build_command_context(
|
|
183
|
+
self, timer: FrameTimer | None
|
|
184
|
+
) -> CommandContext:
|
|
185
|
+
# Build the command execution context.
|
|
186
|
+
if timer:
|
|
187
|
+
timer.mark("command_ctx_start")
|
|
188
|
+
ctx = CommandContext(
|
|
189
|
+
services=self.services,
|
|
190
|
+
managers=self.managers,
|
|
191
|
+
settings=self.game.settings,
|
|
192
|
+
world=self.game.resolve_world(),
|
|
193
|
+
)
|
|
194
|
+
if timer:
|
|
195
|
+
timer.mark("command_ctx_end")
|
|
196
|
+
return ctx
|
|
197
|
+
|
|
198
|
+
def _process_cheats(
|
|
199
|
+
self,
|
|
200
|
+
input_frame: InputFrame,
|
|
201
|
+
ctx: CommandContext,
|
|
202
|
+
timer: FrameTimer | None,
|
|
203
|
+
):
|
|
204
|
+
# Process cheat codes based on the input frame.
|
|
205
|
+
if timer:
|
|
206
|
+
timer.mark("cheats_start")
|
|
207
|
+
self.managers.cheats.process_frame(
|
|
208
|
+
input_frame, context=ctx, queue=self.managers.command_queue
|
|
209
|
+
)
|
|
210
|
+
if timer:
|
|
211
|
+
timer.mark("cheats_end")
|
|
212
|
+
|
|
213
|
+
def _execute_commands(self, ctx: CommandContext, timer: FrameTimer | None):
|
|
214
|
+
# Execute all queued commands.
|
|
215
|
+
if timer:
|
|
216
|
+
timer.mark("cmd_exec_start")
|
|
217
|
+
for cmd in self.managers.command_queue.drain():
|
|
218
|
+
cmd.execute(ctx)
|
|
219
|
+
if timer:
|
|
220
|
+
timer.mark("cmd_exec_end")
|
|
221
|
+
|
|
222
|
+
def _render_frame(self, frame: FrameState, timer: FrameTimer | None):
|
|
223
|
+
# Render the current frame using the render pipeline.
|
|
224
|
+
if timer:
|
|
225
|
+
timer.mark("render_start")
|
|
226
|
+
|
|
227
|
+
vp = self.services.window.get_viewport()
|
|
228
|
+
|
|
229
|
+
frame_packets: list[FramePacket] = []
|
|
230
|
+
for entry in self.managers.scenes.visible_entries():
|
|
231
|
+
scene = entry.scene
|
|
232
|
+
packet = self._packet_cache.get(id(scene))
|
|
233
|
+
if packet is None:
|
|
234
|
+
packet = scene.tick(
|
|
235
|
+
_neutral_input(frame.frame_index, 0.0), 0.0
|
|
236
|
+
)
|
|
237
|
+
self._packet_cache[id(scene)] = packet
|
|
238
|
+
|
|
239
|
+
frame_packets.append(
|
|
240
|
+
FramePacket(
|
|
241
|
+
scene_id=entry.scene_id,
|
|
242
|
+
is_overlay=entry.is_overlay,
|
|
243
|
+
packet=packet,
|
|
244
|
+
)
|
|
245
|
+
)
|
|
246
|
+
|
|
247
|
+
render_ctx = RenderContext(
|
|
248
|
+
viewport=vp,
|
|
249
|
+
debug_overlay=getattr(self.game.settings, "debug_overlay", False),
|
|
250
|
+
frame_ms=frame.dt * 1000.0,
|
|
251
|
+
)
|
|
252
|
+
render_ctx.meta["frame_index"] = frame.frame_index
|
|
253
|
+
render_ctx.meta["time_s"] = frame.time_s
|
|
254
|
+
render_ctx.meta["effects_stack"] = self.effects_stack
|
|
255
|
+
|
|
256
|
+
self.services.render.last_frame_ms = render_ctx.frame_ms
|
|
257
|
+
self.services.render.last_stats = render_ctx.stats
|
|
258
|
+
|
|
259
|
+
self.pipeline.render_frame(self.backend, render_ctx, frame_packets)
|
|
260
|
+
|
|
261
|
+
if timer:
|
|
262
|
+
timer.mark("render_done")
|
|
263
|
+
timer.mark("end_frame_done")
|
|
264
|
+
|
|
265
|
+
def _sleep(self, target_dt: float, dt: float, timer: FrameTimer | None):
|
|
266
|
+
# Sleep to maintain target frame rate if necessary.
|
|
267
|
+
if timer:
|
|
268
|
+
timer.mark("sleep_start")
|
|
269
|
+
if target_dt > 0 and dt < target_dt:
|
|
270
|
+
sleep(target_dt - dt)
|
|
271
|
+
if timer:
|
|
272
|
+
timer.mark("sleep_end")
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Game core module defining the Game class and configuration.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
from __future__ import annotations
|
|
6
|
+
|
|
7
|
+
from dataclasses import dataclass, field
|
|
8
|
+
from time import perf_counter
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
@dataclass
|
|
12
|
+
class FrameState:
|
|
13
|
+
"""
|
|
14
|
+
State of the current frame in the main loop.
|
|
15
|
+
|
|
16
|
+
:ivar frame_index (int): The current frame index.
|
|
17
|
+
:ivar last_time (float): The timestamp of the last frame.
|
|
18
|
+
:ivar time_s (float): The total elapsed time in seconds.
|
|
19
|
+
:ivar dt (float): The delta time since the last frame in seconds.
|
|
20
|
+
"""
|
|
21
|
+
|
|
22
|
+
frame_index: int = 0
|
|
23
|
+
last_time: float = field(default_factory=perf_counter)
|
|
24
|
+
time_s: float = 0.0
|
|
25
|
+
dt: float = 0.0
|
|
26
|
+
|
|
27
|
+
def step_time(self):
|
|
28
|
+
"""Step the time forward by calculating dt."""
|
|
29
|
+
now = perf_counter()
|
|
30
|
+
self.dt = now - self.last_time
|
|
31
|
+
self.last_time = now
|
|
32
|
+
self.time_s += self.dt
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Game core module defining the Game class and configuration.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
from __future__ import annotations
|
|
6
|
+
|
|
7
|
+
from dataclasses import dataclass, field
|
|
8
|
+
|
|
9
|
+
from mini_arcade_core.engine.cheats import CheatManager
|
|
10
|
+
from mini_arcade_core.engine.commands import CommandQueue
|
|
11
|
+
from mini_arcade_core.engine.scenes.scene_manager import SceneAdapter
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
@dataclass
|
|
15
|
+
class EngineManagers:
|
|
16
|
+
"""
|
|
17
|
+
Container for various game managers.
|
|
18
|
+
|
|
19
|
+
:ivar cheats (CheatManager): Manager for handling cheat codes.
|
|
20
|
+
"""
|
|
21
|
+
|
|
22
|
+
cheats: CheatManager = field(default_factory=CheatManager)
|
|
23
|
+
command_queue: CommandQueue = field(default_factory=CommandQueue)
|
|
24
|
+
scenes: SceneAdapter | None = None
|
|
@@ -19,14 +19,12 @@ class RenderStats:
|
|
|
19
19
|
:ivar ops (int): Number of rendering operations executed.
|
|
20
20
|
:ivar draw_groups (int): Number of draw groups processed.
|
|
21
21
|
:ivar renderables (int): Number of renderable objects processed.
|
|
22
|
-
:ivar draw_groups (int): Number of draw groups processed.
|
|
23
22
|
"""
|
|
24
23
|
|
|
25
24
|
packets: int = 0
|
|
26
25
|
ops: int = 0
|
|
27
26
|
draw_groups: int = 0 # approx ok
|
|
28
27
|
renderables: int = 0
|
|
29
|
-
draw_groups: int = 0
|
|
30
28
|
|
|
31
29
|
|
|
32
30
|
@dataclass
|
|
@@ -22,7 +22,7 @@ class Effect(Protocol):
|
|
|
22
22
|
|
|
23
23
|
effect_id: str
|
|
24
24
|
|
|
25
|
-
def apply(self, backend: Backend, ctx: RenderContext)
|
|
25
|
+
def apply(self, backend: Backend, ctx: RenderContext):
|
|
26
26
|
"""
|
|
27
27
|
Apply the effect to the current framebuffer.
|
|
28
28
|
|
|
@@ -75,7 +75,7 @@ class EffectStack:
|
|
|
75
75
|
"""
|
|
76
76
|
return self.enabled and bool(self.active)
|
|
77
77
|
|
|
78
|
-
def toggle(self, effect_id: str)
|
|
78
|
+
def toggle(self, effect_id: str):
|
|
79
79
|
"""
|
|
80
80
|
Toggle an effect on/off.
|
|
81
81
|
|
|
@@ -26,7 +26,7 @@ class CRTEffect:
|
|
|
26
26
|
|
|
27
27
|
# Justification: This is PoC code for v1.
|
|
28
28
|
# pylint: disable=too-many-locals
|
|
29
|
-
def apply(self, backend: Backend, ctx: RenderContext)
|
|
29
|
+
def apply(self, backend: Backend, ctx: RenderContext):
|
|
30
30
|
"""Apply the CRT effect to the current render context."""
|
|
31
31
|
vp = ctx.viewport
|
|
32
32
|
x0, y0 = vp.offset_x, vp.offset_y
|
|
@@ -48,7 +48,7 @@ class CRTEffect:
|
|
|
48
48
|
wobble = float(params.wobble_speed)
|
|
49
49
|
|
|
50
50
|
# Clip to viewport so it works with all viewport modes/resolutions
|
|
51
|
-
backend.set_clip_rect(x0, y0, w, h)
|
|
51
|
+
backend.render.set_clip_rect(x0, y0, w, h)
|
|
52
52
|
|
|
53
53
|
# Scanlines: draw every N lines with low alpha
|
|
54
54
|
# Note: assumes Backend supports alpha in color tuples.
|
|
@@ -61,8 +61,8 @@ class CRTEffect:
|
|
|
61
61
|
for y in range(y0, y0 + h, spacing):
|
|
62
62
|
# shift in pixels, -2..2-ish
|
|
63
63
|
shift = int(2.0 * intensity * sin((y * 0.05) + (t * wobble)))
|
|
64
|
-
backend.draw_line(
|
|
64
|
+
backend.render.draw_line(
|
|
65
65
|
x0 + shift, y, x0 + w + shift, y, color=line_color
|
|
66
66
|
)
|
|
67
67
|
|
|
68
|
-
backend.clear_clip_rect()
|
|
68
|
+
backend.render.clear_clip_rect()
|
|
@@ -26,7 +26,7 @@ class VignetteNoiseEffect:
|
|
|
26
26
|
|
|
27
27
|
# Justification: This is PoC code for v1.
|
|
28
28
|
# pylint: disable=too-many-locals
|
|
29
|
-
def apply(self, backend: Backend, ctx: RenderContext)
|
|
29
|
+
def apply(self, backend: Backend, ctx: RenderContext):
|
|
30
30
|
"""Apply the Vignette + Noise effect to the current render context."""
|
|
31
31
|
vp = ctx.viewport
|
|
32
32
|
x0, y0 = vp.offset_x, vp.offset_y
|
|
@@ -43,7 +43,7 @@ class VignetteNoiseEffect:
|
|
|
43
43
|
if intensity <= 0.0:
|
|
44
44
|
return
|
|
45
45
|
|
|
46
|
-
backend.set_clip_rect(x0, y0, w, h)
|
|
46
|
+
backend.render.set_clip_rect(x0, y0, w, h)
|
|
47
47
|
|
|
48
48
|
# Vignette approximation: draw edge rectangles with increasing alpha.
|
|
49
49
|
# Not a true radial gradient, but good enough for v1.
|
|
@@ -56,13 +56,13 @@ class VignetteNoiseEffect:
|
|
|
56
56
|
color = (0, 0, 0, alpha)
|
|
57
57
|
|
|
58
58
|
# top
|
|
59
|
-
backend.draw_rect(x0, y0, w, t, color=color)
|
|
59
|
+
backend.render.draw_rect(x0, y0, w, t, color=color)
|
|
60
60
|
# bottom
|
|
61
|
-
backend.draw_rect(x0, y0 + h - t, w, t, color=color)
|
|
61
|
+
backend.render.draw_rect(x0, y0 + h - t, w, t, color=color)
|
|
62
62
|
# left
|
|
63
|
-
backend.draw_rect(x0, y0, t, h, color=color)
|
|
63
|
+
backend.render.draw_rect(x0, y0, t, h, color=color)
|
|
64
64
|
# right
|
|
65
|
-
backend.draw_rect(x0 + w - t, y0, t, h, color=color)
|
|
65
|
+
backend.render.draw_rect(x0 + w - t, y0, t, h, color=color)
|
|
66
66
|
|
|
67
67
|
# Noise: sprinkle a few pixels (or tiny 1x1 rects).
|
|
68
68
|
# Use deterministic-ish seed per frame so it doesn't “swim” too much.
|
|
@@ -74,6 +74,6 @@ class VignetteNoiseEffect:
|
|
|
74
74
|
px = x0 + random.randint(0, max(0, w - 1))
|
|
75
75
|
py = y0 + random.randint(0, max(0, h - 1))
|
|
76
76
|
a = random.randint(10, int(50 * intensity) + 10)
|
|
77
|
-
backend.draw_rect(px, py, 1, 1, color=(255, 255, 255, a))
|
|
77
|
+
backend.render.draw_rect(px, py, 1, 1, color=(255, 255, 255, a))
|
|
78
78
|
|
|
79
|
-
backend.clear_clip_rect()
|
|
79
|
+
backend.render.clear_clip_rect()
|
|
@@ -24,7 +24,7 @@ class UIPass:
|
|
|
24
24
|
"""Run the UI render pass."""
|
|
25
25
|
# UI overlays should be screen-space (no world transform / no clip unless you want it)
|
|
26
26
|
backend.clear_viewport_transform()
|
|
27
|
-
backend.clear_clip_rect()
|
|
27
|
+
backend.render.clear_clip_rect()
|
|
28
28
|
|
|
29
29
|
for fp in packets:
|
|
30
30
|
if not fp.is_overlay:
|
|
@@ -41,15 +41,15 @@ class WorldPass:
|
|
|
41
41
|
backend.set_viewport_transform(
|
|
42
42
|
ctx.viewport.offset_x, ctx.viewport.offset_y, ctx.viewport.scale
|
|
43
43
|
)
|
|
44
|
-
backend.set_clip_rect(
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
ctx.viewport.
|
|
48
|
-
ctx.viewport.
|
|
44
|
+
backend.render.set_clip_rect(
|
|
45
|
+
0,
|
|
46
|
+
0,
|
|
47
|
+
ctx.viewport.virtual_w,
|
|
48
|
+
ctx.viewport.virtual_h,
|
|
49
49
|
)
|
|
50
50
|
try:
|
|
51
51
|
for op in packet.ops:
|
|
52
52
|
op(backend)
|
|
53
53
|
finally:
|
|
54
|
-
backend.clear_clip_rect()
|
|
54
|
+
backend.render.clear_clip_rect()
|
|
55
55
|
backend.clear_viewport_transform()
|
|
@@ -19,6 +19,7 @@ from dataclasses import dataclass, field
|
|
|
19
19
|
|
|
20
20
|
from mini_arcade_core.backend import Backend
|
|
21
21
|
from mini_arcade_core.engine.render.context import RenderContext
|
|
22
|
+
from mini_arcade_core.engine.render.frame_packet import FramePacket
|
|
22
23
|
from mini_arcade_core.engine.render.packet import RenderPacket
|
|
23
24
|
from mini_arcade_core.engine.render.passes.base import RenderPass
|
|
24
25
|
from mini_arcade_core.engine.render.passes.begin_frame import BeginFramePass
|
|
@@ -56,10 +57,10 @@ class RenderPipeline:
|
|
|
56
57
|
)
|
|
57
58
|
|
|
58
59
|
def render_frame(
|
|
59
|
-
self, backend: Backend, ctx: RenderContext, packets: list[
|
|
60
|
+
self, backend: Backend, ctx: RenderContext, packets: list[FramePacket]
|
|
60
61
|
):
|
|
61
62
|
"""
|
|
62
|
-
Render a frame using the provided Backend, RenderContext, and list of
|
|
63
|
+
Render a frame using the provided Backend, RenderContext, and list of FramePackets.
|
|
63
64
|
|
|
64
65
|
:param backend: Backend to use for rendering.
|
|
65
66
|
:type backend: Backend
|
|
@@ -67,8 +68,8 @@ class RenderPipeline:
|
|
|
67
68
|
:param ctx: RenderContext containing rendering state.
|
|
68
69
|
:type ctx: RenderContext
|
|
69
70
|
|
|
70
|
-
:param packets: List of
|
|
71
|
-
:type packets: list[
|
|
71
|
+
:param packets: List of FramePackets to render.
|
|
72
|
+
:type packets: list[FramePacket]
|
|
72
73
|
"""
|
|
73
74
|
for p in self.passes:
|
|
74
75
|
p.run(backend, ctx, packets)
|
|
@@ -98,8 +99,8 @@ class RenderPipeline:
|
|
|
98
99
|
)
|
|
99
100
|
|
|
100
101
|
backend.set_clip_rect(
|
|
101
|
-
|
|
102
|
-
|
|
102
|
+
0,
|
|
103
|
+
0,
|
|
103
104
|
viewport_state.viewport_w,
|
|
104
105
|
viewport_state.viewport_h,
|
|
105
106
|
)
|
|
@@ -4,6 +4,7 @@ Viewport management for virtual to screen coordinate transformations.
|
|
|
4
4
|
|
|
5
5
|
from __future__ import annotations
|
|
6
6
|
|
|
7
|
+
import math
|
|
7
8
|
from dataclasses import dataclass
|
|
8
9
|
from enum import Enum
|
|
9
10
|
|
|
@@ -130,10 +131,15 @@ class Viewport:
|
|
|
130
131
|
sy = window_h / self._virtual_h
|
|
131
132
|
scale = min(sx, sy) if self._mode == ViewportMode.FIT else max(sx, sy)
|
|
132
133
|
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
134
|
+
if self._mode == ViewportMode.FIT:
|
|
135
|
+
vw = int(math.floor(self._virtual_w * scale))
|
|
136
|
+
vh = int(math.floor(self._virtual_h * scale))
|
|
137
|
+
else: # FILL
|
|
138
|
+
vw = int(math.ceil(self._virtual_w * scale))
|
|
139
|
+
vh = int(math.ceil(self._virtual_h * scale))
|
|
140
|
+
|
|
141
|
+
ox = (window_w - vw) // 2
|
|
142
|
+
oy = (window_h - vh) // 2
|
|
137
143
|
|
|
138
144
|
self._state = ViewportState(
|
|
139
145
|
virtual_w=self._virtual_w,
|