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.
Files changed (57) hide show
  1. mini_arcade_core/__init__.py +14 -42
  2. mini_arcade_core/backend/__init__.py +1 -2
  3. mini_arcade_core/backend/backend.py +182 -184
  4. mini_arcade_core/backend/types.py +5 -1
  5. mini_arcade_core/engine/commands.py +8 -8
  6. mini_arcade_core/engine/game.py +54 -354
  7. mini_arcade_core/engine/game_config.py +40 -0
  8. mini_arcade_core/engine/gameplay_settings.py +24 -0
  9. mini_arcade_core/engine/loop/config.py +20 -0
  10. mini_arcade_core/engine/loop/hooks.py +77 -0
  11. mini_arcade_core/engine/loop/runner.py +272 -0
  12. mini_arcade_core/engine/loop/state.py +32 -0
  13. mini_arcade_core/engine/managers.py +24 -0
  14. mini_arcade_core/engine/render/context.py +0 -2
  15. mini_arcade_core/engine/render/effects/base.py +2 -2
  16. mini_arcade_core/engine/render/effects/crt.py +4 -4
  17. mini_arcade_core/engine/render/effects/registry.py +1 -1
  18. mini_arcade_core/engine/render/effects/vignette.py +8 -8
  19. mini_arcade_core/engine/render/passes/begin_frame.py +1 -1
  20. mini_arcade_core/engine/render/passes/end_frame.py +1 -1
  21. mini_arcade_core/engine/render/passes/postfx.py +1 -1
  22. mini_arcade_core/engine/render/passes/ui.py +1 -1
  23. mini_arcade_core/engine/render/passes/world.py +6 -6
  24. mini_arcade_core/engine/render/pipeline.py +7 -6
  25. mini_arcade_core/engine/render/viewport.py +10 -4
  26. mini_arcade_core/engine/scenes/models.py +54 -0
  27. mini_arcade_core/engine/scenes/scene_manager.py +213 -0
  28. mini_arcade_core/runtime/audio/audio_adapter.py +4 -3
  29. mini_arcade_core/runtime/audio/audio_port.py +0 -4
  30. mini_arcade_core/runtime/capture/capture_adapter.py +13 -6
  31. mini_arcade_core/runtime/capture/capture_port.py +0 -4
  32. mini_arcade_core/runtime/context.py +8 -6
  33. mini_arcade_core/runtime/scene/scene_query_adapter.py +31 -0
  34. mini_arcade_core/runtime/scene/scene_query_port.py +38 -0
  35. mini_arcade_core/runtime/services.py +3 -2
  36. mini_arcade_core/runtime/window/window_adapter.py +43 -41
  37. mini_arcade_core/runtime/window/window_port.py +3 -17
  38. mini_arcade_core/scenes/debug_overlay.py +5 -4
  39. mini_arcade_core/scenes/registry.py +11 -1
  40. mini_arcade_core/scenes/sim_scene.py +14 -14
  41. mini_arcade_core/ui/menu.py +54 -16
  42. mini_arcade_core/utils/__init__.py +2 -1
  43. mini_arcade_core/utils/logging.py +47 -18
  44. mini_arcade_core/utils/profiler.py +283 -0
  45. {mini_arcade_core-1.1.1.dist-info → mini_arcade_core-1.2.0.dist-info}/METADATA +1 -1
  46. mini_arcade_core-1.2.0.dist-info/RECORD +92 -0
  47. {mini_arcade_core-1.1.1.dist-info → mini_arcade_core-1.2.0.dist-info}/WHEEL +1 -1
  48. mini_arcade_core/managers/inputs.py +0 -284
  49. mini_arcade_core/runtime/scene/scene_adapter.py +0 -125
  50. mini_arcade_core/runtime/scene/scene_port.py +0 -170
  51. mini_arcade_core/sim/protocols.py +0 -41
  52. mini_arcade_core/sim/runner.py +0 -222
  53. mini_arcade_core-1.1.1.dist-info/RECORD +0 -85
  54. /mini_arcade_core/{managers → engine}/cheats.py +0 -0
  55. /mini_arcade_core/{managers → engine/loop}/__init__.py +0 -0
  56. /mini_arcade_core/{sim → engine/scenes}/__init__.py +0 -0
  57. {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) -> None:
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) -> None:
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) -> None:
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()
@@ -19,7 +19,7 @@ class EffectRegistry:
19
19
 
20
20
  _effects: dict[str, Effect] = field(default_factory=dict)
21
21
 
22
- def register(self, effect: Effect) -> None:
22
+ def register(self, effect: Effect):
23
23
  """
24
24
  Register a new effect in the registry.
25
25
 
@@ -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) -> None:
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,4 +24,4 @@ class BeginFramePass:
24
24
  self, backend: Backend, ctx: RenderContext, packets: list[RenderPacket]
25
25
  ):
26
26
  """Run the begin frame pass."""
27
- backend.begin_frame()
27
+ backend.render.begin_frame()
@@ -25,4 +25,4 @@ class EndFramePass:
25
25
  ):
26
26
  """Run the end frame pass."""
27
27
  # Signal the end of the frame to the backend
28
- backend.end_frame()
28
+ backend.render.end_frame()
@@ -35,7 +35,7 @@ class PostFXPass:
35
35
 
36
36
  # Screen space: no transforms
37
37
  backend.clear_viewport_transform()
38
- backend.clear_clip_rect()
38
+ backend.render.clear_clip_rect()
39
39
 
40
40
  reg = self.registry
41
41
  if reg is None:
@@ -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
- ctx.viewport.offset_x,
46
- ctx.viewport.offset_y,
47
- ctx.viewport.viewport_w,
48
- ctx.viewport.viewport_h,
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[RenderPacket]
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 RenderPackets.
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 RenderPackets to render.
71
- :type packets: list[RenderPacket]
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
- viewport_state.offset_x,
102
- viewport_state.offset_y,
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
- vw = int(round(self._virtual_w * scale))
134
- vh = int(round(self._virtual_h * scale))
135
- ox = int(round((window_w - vw) / 2))
136
- oy = int(round((window_h - vh) / 2))
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,