mini-arcade-core 1.0.1__tar.gz → 1.1.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (81) hide show
  1. {mini_arcade_core-1.0.1 → mini_arcade_core-1.1.0}/PKG-INFO +1 -1
  2. {mini_arcade_core-1.0.1 → mini_arcade_core-1.1.0}/pyproject.toml +1 -1
  3. {mini_arcade_core-1.0.1 → mini_arcade_core-1.1.0}/src/mini_arcade_core/engine/commands.py +30 -0
  4. {mini_arcade_core-1.0.1 → mini_arcade_core-1.1.0}/src/mini_arcade_core/engine/game.py +31 -6
  5. mini_arcade_core-1.1.0/src/mini_arcade_core/engine/render/context.py +48 -0
  6. mini_arcade_core-1.1.0/src/mini_arcade_core/engine/render/frame_packet.py +26 -0
  7. mini_arcade_core-1.1.0/src/mini_arcade_core/engine/render/passes/base.py +37 -0
  8. mini_arcade_core-1.1.0/src/mini_arcade_core/engine/render/passes/begin_frame.py +27 -0
  9. mini_arcade_core-1.1.0/src/mini_arcade_core/engine/render/passes/end_frame.py +28 -0
  10. mini_arcade_core-1.1.0/src/mini_arcade_core/engine/render/passes/lighting.py +28 -0
  11. mini_arcade_core-1.1.0/src/mini_arcade_core/engine/render/passes/postfx.py +28 -0
  12. mini_arcade_core-1.1.0/src/mini_arcade_core/engine/render/passes/ui.py +41 -0
  13. mini_arcade_core-1.1.0/src/mini_arcade_core/engine/render/passes/world.py +55 -0
  14. mini_arcade_core-1.1.0/src/mini_arcade_core/engine/render/pipeline.py +112 -0
  15. mini_arcade_core-1.1.0/src/mini_arcade_core/engine/render/render_service.py +22 -0
  16. mini_arcade_core-1.1.0/src/mini_arcade_core/runtime/render/render_port.py +22 -0
  17. {mini_arcade_core-1.0.1 → mini_arcade_core-1.1.0}/src/mini_arcade_core/runtime/scene/scene_adapter.py +29 -1
  18. {mini_arcade_core-1.0.1 → mini_arcade_core-1.1.0}/src/mini_arcade_core/runtime/scene/scene_port.py +21 -0
  19. {mini_arcade_core-1.0.1 → mini_arcade_core-1.1.0}/src/mini_arcade_core/runtime/services.py +2 -0
  20. mini_arcade_core-1.1.0/src/mini_arcade_core/scenes/debug_overlay.py +70 -0
  21. {mini_arcade_core-1.0.1 → mini_arcade_core-1.1.0}/src/mini_arcade_core/scenes/registry.py +10 -10
  22. mini_arcade_core-1.1.0/src/mini_arcade_core/spaces/d2/__init__.py +0 -0
  23. mini_arcade_core-1.1.0/src/mini_arcade_core/ui/__init__.py +0 -0
  24. mini_arcade_core-1.0.1/src/mini_arcade_core/engine/render/pipeline.py +0 -63
  25. {mini_arcade_core-1.0.1 → mini_arcade_core-1.1.0}/LICENSE +0 -0
  26. {mini_arcade_core-1.0.1 → mini_arcade_core-1.1.0}/README.md +0 -0
  27. {mini_arcade_core-1.0.1 → mini_arcade_core-1.1.0}/src/mini_arcade_core/__init__.py +0 -0
  28. {mini_arcade_core-1.0.1 → mini_arcade_core-1.1.0}/src/mini_arcade_core/backend/__init__.py +0 -0
  29. {mini_arcade_core-1.0.1 → mini_arcade_core-1.1.0}/src/mini_arcade_core/backend/backend.py +0 -0
  30. {mini_arcade_core-1.0.1 → mini_arcade_core-1.1.0}/src/mini_arcade_core/backend/events.py +0 -0
  31. {mini_arcade_core-1.0.1 → mini_arcade_core-1.1.0}/src/mini_arcade_core/backend/keys.py +0 -0
  32. {mini_arcade_core-1.0.1 → mini_arcade_core-1.1.0}/src/mini_arcade_core/backend/sdl_map.py +0 -0
  33. {mini_arcade_core-1.0.1 → mini_arcade_core-1.1.0}/src/mini_arcade_core/backend/types.py +0 -0
  34. {mini_arcade_core-1.0.1 → mini_arcade_core-1.1.0}/src/mini_arcade_core/bus.py +0 -0
  35. {mini_arcade_core-1.0.1 → mini_arcade_core-1.1.0}/src/mini_arcade_core/engine/__init__.py +0 -0
  36. {mini_arcade_core-1.0.1 → mini_arcade_core-1.1.0}/src/mini_arcade_core/engine/render/__init__.py +0 -0
  37. {mini_arcade_core-1.0.1 → mini_arcade_core-1.1.0}/src/mini_arcade_core/engine/render/packet.py +0 -0
  38. {mini_arcade_core-1.0.1/src/mini_arcade_core/managers → mini_arcade_core-1.1.0/src/mini_arcade_core/engine/render/passes}/__init__.py +0 -0
  39. {mini_arcade_core-1.0.1 → mini_arcade_core-1.1.0}/src/mini_arcade_core/engine/render/viewport.py +0 -0
  40. {mini_arcade_core-1.0.1/src/mini_arcade_core/runtime → mini_arcade_core-1.1.0/src/mini_arcade_core/managers}/__init__.py +0 -0
  41. {mini_arcade_core-1.0.1 → mini_arcade_core-1.1.0}/src/mini_arcade_core/managers/cheats.py +0 -0
  42. {mini_arcade_core-1.0.1 → mini_arcade_core-1.1.0}/src/mini_arcade_core/managers/inputs.py +0 -0
  43. {mini_arcade_core-1.0.1/src/mini_arcade_core/runtime/audio → mini_arcade_core-1.1.0/src/mini_arcade_core/runtime}/__init__.py +0 -0
  44. {mini_arcade_core-1.0.1/src/mini_arcade_core/runtime/capture → mini_arcade_core-1.1.0/src/mini_arcade_core/runtime/audio}/__init__.py +0 -0
  45. {mini_arcade_core-1.0.1 → mini_arcade_core-1.1.0}/src/mini_arcade_core/runtime/audio/audio_adapter.py +0 -0
  46. {mini_arcade_core-1.0.1 → mini_arcade_core-1.1.0}/src/mini_arcade_core/runtime/audio/audio_port.py +0 -0
  47. {mini_arcade_core-1.0.1/src/mini_arcade_core/runtime/file → mini_arcade_core-1.1.0/src/mini_arcade_core/runtime/capture}/__init__.py +0 -0
  48. {mini_arcade_core-1.0.1 → mini_arcade_core-1.1.0}/src/mini_arcade_core/runtime/capture/capture_adapter.py +0 -0
  49. {mini_arcade_core-1.0.1 → mini_arcade_core-1.1.0}/src/mini_arcade_core/runtime/capture/capture_port.py +0 -0
  50. {mini_arcade_core-1.0.1 → mini_arcade_core-1.1.0}/src/mini_arcade_core/runtime/context.py +0 -0
  51. {mini_arcade_core-1.0.1/src/mini_arcade_core/runtime/input → mini_arcade_core-1.1.0/src/mini_arcade_core/runtime/file}/__init__.py +0 -0
  52. {mini_arcade_core-1.0.1 → mini_arcade_core-1.1.0}/src/mini_arcade_core/runtime/file/file_adapter.py +0 -0
  53. {mini_arcade_core-1.0.1 → mini_arcade_core-1.1.0}/src/mini_arcade_core/runtime/file/file_port.py +0 -0
  54. {mini_arcade_core-1.0.1/src/mini_arcade_core/runtime/scene → mini_arcade_core-1.1.0/src/mini_arcade_core/runtime/input}/__init__.py +0 -0
  55. {mini_arcade_core-1.0.1 → mini_arcade_core-1.1.0}/src/mini_arcade_core/runtime/input/input_adapter.py +0 -0
  56. {mini_arcade_core-1.0.1 → mini_arcade_core-1.1.0}/src/mini_arcade_core/runtime/input/input_port.py +0 -0
  57. {mini_arcade_core-1.0.1 → mini_arcade_core-1.1.0}/src/mini_arcade_core/runtime/input_frame.py +0 -0
  58. {mini_arcade_core-1.0.1/src/mini_arcade_core/runtime/window → mini_arcade_core-1.1.0/src/mini_arcade_core/runtime/render}/__init__.py +0 -0
  59. {mini_arcade_core-1.0.1/src/mini_arcade_core/scenes → mini_arcade_core-1.1.0/src/mini_arcade_core/runtime/scene}/__init__.py +0 -0
  60. {mini_arcade_core-1.0.1/src/mini_arcade_core/scenes/systems → mini_arcade_core-1.1.0/src/mini_arcade_core/runtime/window}/__init__.py +0 -0
  61. {mini_arcade_core-1.0.1 → mini_arcade_core-1.1.0}/src/mini_arcade_core/runtime/window/window_adapter.py +0 -0
  62. {mini_arcade_core-1.0.1 → mini_arcade_core-1.1.0}/src/mini_arcade_core/runtime/window/window_port.py +0 -0
  63. {mini_arcade_core-1.0.1/src/mini_arcade_core/sim → mini_arcade_core-1.1.0/src/mini_arcade_core/scenes}/__init__.py +0 -0
  64. {mini_arcade_core-1.0.1 → mini_arcade_core-1.1.0}/src/mini_arcade_core/scenes/autoreg.py +0 -0
  65. {mini_arcade_core-1.0.1 → mini_arcade_core-1.1.0}/src/mini_arcade_core/scenes/sim_scene.py +0 -0
  66. {mini_arcade_core-1.0.1/src/mini_arcade_core/spaces → mini_arcade_core-1.1.0/src/mini_arcade_core/scenes/systems}/__init__.py +0 -0
  67. {mini_arcade_core-1.0.1 → mini_arcade_core-1.1.0}/src/mini_arcade_core/scenes/systems/base_system.py +0 -0
  68. {mini_arcade_core-1.0.1 → mini_arcade_core-1.1.0}/src/mini_arcade_core/scenes/systems/system_pipeline.py +0 -0
  69. {mini_arcade_core-1.0.1/src/mini_arcade_core/spaces/d2 → mini_arcade_core-1.1.0/src/mini_arcade_core/sim}/__init__.py +0 -0
  70. {mini_arcade_core-1.0.1 → mini_arcade_core-1.1.0}/src/mini_arcade_core/sim/protocols.py +0 -0
  71. {mini_arcade_core-1.0.1 → mini_arcade_core-1.1.0}/src/mini_arcade_core/sim/runner.py +0 -0
  72. {mini_arcade_core-1.0.1/src/mini_arcade_core/ui → mini_arcade_core-1.1.0/src/mini_arcade_core/spaces}/__init__.py +0 -0
  73. {mini_arcade_core-1.0.1 → mini_arcade_core-1.1.0}/src/mini_arcade_core/spaces/d2/boundaries2d.py +0 -0
  74. {mini_arcade_core-1.0.1 → mini_arcade_core-1.1.0}/src/mini_arcade_core/spaces/d2/collision2d.py +0 -0
  75. {mini_arcade_core-1.0.1 → mini_arcade_core-1.1.0}/src/mini_arcade_core/spaces/d2/geometry2d.py +0 -0
  76. {mini_arcade_core-1.0.1 → mini_arcade_core-1.1.0}/src/mini_arcade_core/spaces/d2/kinematics2d.py +0 -0
  77. {mini_arcade_core-1.0.1 → mini_arcade_core-1.1.0}/src/mini_arcade_core/spaces/d2/physics2d.py +0 -0
  78. {mini_arcade_core-1.0.1 → mini_arcade_core-1.1.0}/src/mini_arcade_core/ui/menu.py +0 -0
  79. {mini_arcade_core-1.0.1 → mini_arcade_core-1.1.0}/src/mini_arcade_core/utils/__init__.py +0 -0
  80. {mini_arcade_core-1.0.1 → mini_arcade_core-1.1.0}/src/mini_arcade_core/utils/deprecated_decorator.py +0 -0
  81. {mini_arcade_core-1.0.1 → mini_arcade_core-1.1.0}/src/mini_arcade_core/utils/logging.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: mini-arcade-core
3
- Version: 1.0.1
3
+ Version: 1.1.0
4
4
  Summary: Tiny scene-based game loop core for small arcade games.
5
5
  License: Copyright (c) 2025 Santiago Rincón
6
6
 
@@ -4,7 +4,7 @@ build-backend = "poetry.core.masonry.api"
4
4
 
5
5
  [project]
6
6
  name = "mini-arcade-core"
7
- version = "1.0.1"
7
+ version = "1.1.0"
8
8
  description = "Tiny scene-based game loop core for small arcade games."
9
9
  authors = [
10
10
  { name = "Santiago Rincon", email = "rincores@gmail.com" },
@@ -7,6 +7,8 @@ from __future__ import annotations
7
7
  from dataclasses import dataclass, field
8
8
  from typing import TYPE_CHECKING, List, Optional, Protocol, TypeVar
9
9
 
10
+ from mini_arcade_core.runtime.scene.scene_port import ScenePolicy
11
+
10
12
  if TYPE_CHECKING:
11
13
  from mini_arcade_core.runtime.services import RuntimeServices
12
14
 
@@ -167,3 +169,31 @@ class ChangeSceneCommand(Command):
167
169
  context: CommandContext,
168
170
  ):
169
171
  context.services.scenes.change(self.scene_id)
172
+
173
+
174
+ @dataclass(frozen=True)
175
+ class ToggleDebugOverlayCommand(Command):
176
+ """
177
+ Toggle the debug overlay scene.
178
+
179
+ :cvar DEBUG_OVERLAY_ID: str: Identifier for the debug overlay scene.
180
+ """
181
+
182
+ DEBUG_OVERLAY_ID = "debug_overlay"
183
+
184
+ def execute(self, context: CommandContext):
185
+ scenes = context.services.scenes
186
+ if scenes.has_scene(self.DEBUG_OVERLAY_ID):
187
+ scenes.remove_scene(self.DEBUG_OVERLAY_ID)
188
+ return
189
+
190
+ scenes.push(
191
+ self.DEBUG_OVERLAY_ID,
192
+ as_overlay=True,
193
+ policy=ScenePolicy(
194
+ blocks_update=False,
195
+ blocks_input=False,
196
+ is_opaque=False,
197
+ receives_input=False,
198
+ ),
199
+ )
@@ -10,13 +10,18 @@ from typing import Dict, Literal
10
10
 
11
11
  from mini_arcade_core.backend import Backend, WindowSettings
12
12
  from mini_arcade_core.backend.events import EventType
13
+ from mini_arcade_core.backend.keys import Key
13
14
  from mini_arcade_core.engine.commands import (
14
15
  CommandContext,
15
16
  CommandQueue,
16
17
  QuitCommand,
18
+ ToggleDebugOverlayCommand,
17
19
  )
20
+ from mini_arcade_core.engine.render.context import RenderContext
21
+ from mini_arcade_core.engine.render.frame_packet import FramePacket
18
22
  from mini_arcade_core.engine.render.packet import RenderPacket
19
23
  from mini_arcade_core.engine.render.pipeline import RenderPipeline
24
+ from mini_arcade_core.engine.render.render_service import RenderService
20
25
  from mini_arcade_core.managers.cheats import CheatManager
21
26
  from mini_arcade_core.runtime.audio.audio_adapter import SDLAudioAdapter
22
27
  from mini_arcade_core.runtime.capture.capture_adapter import CaptureAdapter
@@ -186,6 +191,7 @@ class Game:
186
191
  files=LocalFilesAdapter(),
187
192
  capture=CaptureAdapter(self.backend),
188
193
  input=InputAdapter(),
194
+ render=RenderService(),
189
195
  )
190
196
 
191
197
  self.command_queue = CommandQueue()
@@ -248,6 +254,9 @@ class Game:
248
254
  w, h = e.size
249
255
  logger.debug(f"Window resized event: {w}x{h}")
250
256
  self.services.window.on_window_resized(w, h)
257
+ # if F1 pressed, toggle debug overlay
258
+ if e.type == EventType.KEYDOWN and e.key == Key.F1:
259
+ self.command_queue.push(ToggleDebugOverlayCommand())
251
260
  timer.mark("events_polled")
252
261
 
253
262
  input_frame = self.services.input.build(events, frame_index, dt)
@@ -299,23 +308,39 @@ class Game:
299
308
  cmd.execute(command_context)
300
309
  timer.mark("cmd_exec_end")
301
310
 
311
+ # ---------------- TO REPLACE WITH RENDERING PIPELINE ----------------
302
312
  timer.mark("render_start")
303
- backend.begin_frame()
304
- timer.mark("begin_frame_done")
305
313
 
306
314
  vp = self.services.window.get_viewport()
315
+
316
+ # gather visible packets
317
+ frame_packets: list[RenderPacket] = []
307
318
  for entry in self.services.scenes.visible_entries():
308
319
  scene = entry.scene
309
320
  packet = packet_cache.get(id(scene))
310
321
  if packet is None:
311
- # bootstrap (first frame visible but not updated)
312
322
  packet = scene.tick(_neutral_input(frame_index, 0.0), 0.0)
313
323
  packet_cache[id(scene)] = packet
324
+ frame_packets.append(
325
+ FramePacket(
326
+ scene_id=entry.scene_id,
327
+ is_overlay=entry.is_overlay,
328
+ packet=packet,
329
+ )
330
+ )
331
+
332
+ render_ctx = RenderContext(
333
+ viewport=vp,
334
+ debug_overlay=getattr(self.settings, "debug_overlay", False),
335
+ frame_ms=dt * 1000.0,
336
+ )
314
337
 
315
- pipeline.draw_packet(backend, packet, vp)
338
+ self.services.render.last_frame_ms = render_ctx.frame_ms
339
+ self.services.render.last_stats = render_ctx.stats
340
+ pipeline.render_frame(backend, render_ctx, frame_packets)
316
341
 
317
- timer.mark("draw_done")
318
- backend.end_frame()
342
+ timer.mark("render_done")
343
+ # ---------------- END RENDERING PIPELINE ----------------------------
319
344
  timer.mark("end_frame_done")
320
345
 
321
346
  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)
@@ -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
@@ -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,28 @@
1
+ """
2
+ Post-processing effects 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 PostFXPass:
14
+ """
15
+ PostFX Render Pass.
16
+ This pass handles post-processing effects like CRT simulation.
17
+ """
18
+
19
+ name: str = "PostFXPass"
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 post-processing effects render pass."""
27
+ # hook/no-op for now (CRT later)
28
+ return
@@ -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()
@@ -0,0 +1,112 @@
1
+ """
2
+ Render pipeline module.
3
+ Defines the RenderPipeline class for rendering RenderPackets.
4
+ """
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
+
16
+ from __future__ import annotations
17
+
18
+ from dataclasses import dataclass, field
19
+
20
+ from mini_arcade_core.backend import Backend
21
+ from mini_arcade_core.engine.render.context import RenderContext
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
30
+ from mini_arcade_core.engine.render.viewport import ViewportState
31
+
32
+
33
+ @dataclass
34
+ class RenderPipeline:
35
+ """
36
+ Minimal pipeline for v1.
37
+
38
+ Later you can expand this into passes:
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.
45
+ """
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
+
76
+ def draw_packet(
77
+ self,
78
+ backend: Backend,
79
+ packet: RenderPacket,
80
+ viewport_state: ViewportState,
81
+ ):
82
+ """
83
+ Draw the given RenderPacket using the provided Backend.
84
+
85
+ :param backend: Backend to use for drawing.
86
+ :type backend: Backend
87
+
88
+ :param packet: RenderPacket to draw.
89
+ :type packet: RenderPacket
90
+ """
91
+ if not packet:
92
+ return
93
+
94
+ backend.set_viewport_transform(
95
+ viewport_state.offset_x,
96
+ viewport_state.offset_y,
97
+ viewport_state.scale,
98
+ )
99
+
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
+ )
106
+
107
+ try:
108
+ for op in packet.ops:
109
+ op(backend)
110
+ finally:
111
+ backend.clear_clip_rect()
112
+ backend.clear_viewport_transform()
@@ -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()
@@ -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
@@ -94,4 +94,32 @@ class SceneAdapter(ScenePort):
94
94
 
95
95
  def input_entry(self):
96
96
  vis = self.visible_entries()
97
- return vis[-1] if vis else None
97
+ if not vis:
98
+ return None
99
+
100
+ # If some scene blocks input, only scenes at/above it can receive.
101
+ start_idx = 0
102
+ for idx in range(len(vis) - 1, -1, -1):
103
+ if vis[idx].policy.blocks_input:
104
+ start_idx = idx
105
+ break
106
+
107
+ candidates = vis[start_idx:]
108
+
109
+ # Pick the top-most candidate that actually receives input.
110
+ for entry in reversed(candidates):
111
+ if entry.policy.receives_input:
112
+ return entry
113
+
114
+ return None
115
+
116
+ def has_scene(self, scene_id: str) -> bool:
117
+ return any(item.entry.scene_id == scene_id for item in self._stack)
118
+
119
+ def remove_scene(self, scene_id: str):
120
+ # remove first match from top (overlay is usually near top)
121
+ for i in range(len(self._stack) - 1, -1, -1):
122
+ if self._stack[i].entry.scene_id == scene_id:
123
+ item = self._stack.pop(i)
124
+ item.entry.scene.on_exit()
125
+ return
@@ -23,11 +23,13 @@ class ScenePolicy:
23
23
  blocks_update: if True, scenes below do not tick/update (pause modal)
24
24
  blocks_input: if True, scenes below do not receive input
25
25
  is_opaque: if True, scenes below are not rendered
26
+ receives_input: if True, scene can receive input
26
27
  """
27
28
 
28
29
  blocks_update: bool = False
29
30
  blocks_input: bool = False
30
31
  is_opaque: bool = False
32
+ receives_input: bool = True
31
33
 
32
34
 
33
35
  @dataclass(frozen=True)
@@ -147,3 +149,22 @@ class ScenePort:
147
149
  :return: The SceneEntry that receives input, or None if no scenes are active.
148
150
  :rtype: SceneEntry | None
149
151
  """
152
+
153
+ def has_scene(self, scene_id: str) -> bool:
154
+ """
155
+ Check if a scene with the given ID exists in the stack.
156
+
157
+ :param scene_id: Identifier of the scene to check.
158
+ :type scene_id: str
159
+
160
+ :return: True if the scene exists in the stack, False otherwise.
161
+ :rtype: bool
162
+ """
163
+
164
+ def remove_scene(self, scene_id: str):
165
+ """
166
+ Remove a scene with the given ID from the stack.
167
+
168
+ :param scene_id: Identifier of the scene to remove.
169
+ :type scene_id: str
170
+ """
@@ -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
@@ -0,0 +1,70 @@
1
+ """
2
+ Debug overlay scene that displays FPS, window size, and scene stack information.
3
+ """
4
+
5
+ from __future__ import annotations
6
+
7
+ from mini_arcade_core.engine.render.packet import RenderPacket
8
+ from mini_arcade_core.runtime.context import RuntimeContext
9
+ from mini_arcade_core.runtime.input_frame import InputFrame
10
+ from mini_arcade_core.scenes.autoreg import register_scene
11
+ from mini_arcade_core.sim.protocols import SimScene
12
+
13
+
14
+ @register_scene("debug_overlay")
15
+ class DebugOverlayScene(SimScene):
16
+ """
17
+ A debug overlay scene that displays FPS, window size, and scene stack information.
18
+ """
19
+
20
+ def __init__(self, ctx: RuntimeContext):
21
+ super().__init__(ctx)
22
+ self._accum = 0.0
23
+ self._frames = 0
24
+ self._fps = 0.0
25
+
26
+ def tick(self, input_frame: InputFrame, dt: float) -> RenderPacket:
27
+ self._accum += dt
28
+ self._frames += 1
29
+ if self._accum >= 0.5:
30
+ self._fps = self._frames / self._accum
31
+ self._accum = 0.0
32
+ self._frames = 0
33
+
34
+ services = (
35
+ self.context.services
36
+ ) # or ctx.services (depends on your scene base)
37
+ # Justification: type checker can't infer type here
38
+ # pylint: disable=assignment-from-no-return
39
+ vp = services.window.get_viewport()
40
+ stack = services.scenes.visible_entries()
41
+ # pylint: enable=assignment-from-no-return
42
+ rs = services.render
43
+ lines = [
44
+ f"FPS: {self._fps:5.1f}",
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}",
49
+ f"virtual: {vp.virtual_w}x{vp.virtual_h}",
50
+ f"window: {vp.window_w}x{vp.window_h}",
51
+ f"scale: {vp.scale:.3f}",
52
+ f"offset: ({vp.offset_x},{vp.offset_y})",
53
+ "stack:",
54
+ ]
55
+ for e in stack:
56
+ lines.append(f" - {e.scene_id} overlay={e.is_overlay}")
57
+
58
+ def draw(backend):
59
+ # translucent background panel
60
+ backend.draw_rect(
61
+ 8, 8, 360, 18 * (len(lines) + 1), color=(0, 0, 0, 0.65)
62
+ )
63
+ y = 14
64
+ for line in lines:
65
+ backend.draw_text(
66
+ 16, y, line, color=(255, 255, 255), font_size=14
67
+ )
68
+ y += 18
69
+
70
+ return RenderPacket(ops=[draw])
@@ -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.engine.commands import CommandQueue
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):
@@ -93,22 +92,23 @@ class SceneRegistry:
93
92
  for scene_id, cls in catalog.items():
94
93
  self.register_cls(scene_id, cls)
95
94
 
96
- def discover(self, package: str) -> "SceneRegistry":
95
+ def discover(self, *packages: str) -> "SceneRegistry":
97
96
  """
98
97
  Import all modules in a package so @scene decorators run.
99
98
 
100
- :param package: The package name to scan for scene modules.
101
- :type package: str
99
+ :param packages: The package names to scan for scene modules.
100
+ :type packages: str
102
101
 
103
102
  :return: The SceneRegistry instance (for chaining).
104
103
  :rtype: SceneRegistry
105
104
  """
106
- pkg = importlib.import_module(package)
107
- if not hasattr(pkg, "__path__"):
108
- return self # not a package
105
+ for package in packages:
106
+ pkg = importlib.import_module(package)
107
+ if not hasattr(pkg, "__path__"):
108
+ continue
109
109
 
110
- for mod in pkgutil.walk_packages(pkg.__path__, pkg.__name__ + "."):
111
- importlib.import_module(mod.name)
110
+ for mod in pkgutil.walk_packages(pkg.__path__, pkg.__name__ + "."):
111
+ importlib.import_module(mod.name)
112
112
 
113
113
  self.load_catalog(snapshot())
114
114
  return self
@@ -1,63 +0,0 @@
1
- """
2
- Render pipeline module.
3
- Defines the RenderPipeline class for rendering RenderPackets.
4
- """
5
-
6
- from __future__ import annotations
7
-
8
- from dataclasses import dataclass
9
-
10
- from mini_arcade_core.backend import Backend
11
- from mini_arcade_core.engine.render.packet import RenderPacket
12
- from mini_arcade_core.engine.render.viewport import ViewportState
13
-
14
-
15
- @dataclass
16
- class RenderPipeline:
17
- """
18
- Minimal pipeline for v1.
19
-
20
- Later you can expand this into passes:
21
- - build draw list
22
- - cull
23
- - sort
24
- - backend draw pass
25
- """
26
-
27
- def draw_packet(
28
- self,
29
- backend: Backend,
30
- packet: RenderPacket,
31
- viewport_state: ViewportState,
32
- ):
33
- """
34
- Draw the given RenderPacket using the provided Backend.
35
-
36
- :param backend: Backend to use for drawing.
37
- :type backend: Backend
38
-
39
- :param packet: RenderPacket to draw.
40
- :type packet: RenderPacket
41
- """
42
- if not packet:
43
- return
44
-
45
- backend.set_viewport_transform(
46
- viewport_state.offset_x,
47
- viewport_state.offset_y,
48
- viewport_state.scale,
49
- )
50
-
51
- # backend.set_clip_rect(
52
- # viewport_state.offset_x,
53
- # viewport_state.offset_y,
54
- # viewport_state.viewport_w,
55
- # viewport_state.viewport_h,
56
- # )
57
-
58
- try:
59
- for op in packet.ops:
60
- op(backend)
61
- finally:
62
- backend.clear_clip_rect()
63
- backend.clear_viewport_transform()