mini-arcade-core 1.0.2__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.2 → mini_arcade_core-1.1.0}/PKG-INFO +1 -1
  2. {mini_arcade_core-1.0.2 → mini_arcade_core-1.1.0}/pyproject.toml +1 -1
  3. {mini_arcade_core-1.0.2 → mini_arcade_core-1.1.0}/src/mini_arcade_core/engine/commands.py +6 -2
  4. {mini_arcade_core-1.0.2 → mini_arcade_core-1.1.0}/src/mini_arcade_core/engine/game.py +26 -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.2 → mini_arcade_core-1.1.0}/src/mini_arcade_core/runtime/scene/scene_adapter.py +1 -1
  18. {mini_arcade_core-1.0.2 → mini_arcade_core-1.1.0}/src/mini_arcade_core/runtime/scene/scene_port.py +1 -1
  19. {mini_arcade_core-1.0.2 → mini_arcade_core-1.1.0}/src/mini_arcade_core/runtime/services.py +2 -0
  20. {mini_arcade_core-1.0.2 → mini_arcade_core-1.1.0}/src/mini_arcade_core/scenes/debug_overlay.py +4 -1
  21. {mini_arcade_core-1.0.2 → mini_arcade_core-1.1.0}/src/mini_arcade_core/scenes/registry.py +1 -2
  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.2/src/mini_arcade_core/engine/render/pipeline.py +0 -63
  25. {mini_arcade_core-1.0.2 → mini_arcade_core-1.1.0}/LICENSE +0 -0
  26. {mini_arcade_core-1.0.2 → mini_arcade_core-1.1.0}/README.md +0 -0
  27. {mini_arcade_core-1.0.2 → mini_arcade_core-1.1.0}/src/mini_arcade_core/__init__.py +0 -0
  28. {mini_arcade_core-1.0.2 → mini_arcade_core-1.1.0}/src/mini_arcade_core/backend/__init__.py +0 -0
  29. {mini_arcade_core-1.0.2 → mini_arcade_core-1.1.0}/src/mini_arcade_core/backend/backend.py +0 -0
  30. {mini_arcade_core-1.0.2 → mini_arcade_core-1.1.0}/src/mini_arcade_core/backend/events.py +0 -0
  31. {mini_arcade_core-1.0.2 → mini_arcade_core-1.1.0}/src/mini_arcade_core/backend/keys.py +0 -0
  32. {mini_arcade_core-1.0.2 → mini_arcade_core-1.1.0}/src/mini_arcade_core/backend/sdl_map.py +0 -0
  33. {mini_arcade_core-1.0.2 → mini_arcade_core-1.1.0}/src/mini_arcade_core/backend/types.py +0 -0
  34. {mini_arcade_core-1.0.2 → mini_arcade_core-1.1.0}/src/mini_arcade_core/bus.py +0 -0
  35. {mini_arcade_core-1.0.2 → mini_arcade_core-1.1.0}/src/mini_arcade_core/engine/__init__.py +0 -0
  36. {mini_arcade_core-1.0.2 → mini_arcade_core-1.1.0}/src/mini_arcade_core/engine/render/__init__.py +0 -0
  37. {mini_arcade_core-1.0.2 → mini_arcade_core-1.1.0}/src/mini_arcade_core/engine/render/packet.py +0 -0
  38. {mini_arcade_core-1.0.2/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.2 → mini_arcade_core-1.1.0}/src/mini_arcade_core/engine/render/viewport.py +0 -0
  40. {mini_arcade_core-1.0.2/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.2 → mini_arcade_core-1.1.0}/src/mini_arcade_core/managers/cheats.py +0 -0
  42. {mini_arcade_core-1.0.2 → mini_arcade_core-1.1.0}/src/mini_arcade_core/managers/inputs.py +0 -0
  43. {mini_arcade_core-1.0.2/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.2/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.2 → mini_arcade_core-1.1.0}/src/mini_arcade_core/runtime/audio/audio_adapter.py +0 -0
  46. {mini_arcade_core-1.0.2 → mini_arcade_core-1.1.0}/src/mini_arcade_core/runtime/audio/audio_port.py +0 -0
  47. {mini_arcade_core-1.0.2/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.2 → mini_arcade_core-1.1.0}/src/mini_arcade_core/runtime/capture/capture_adapter.py +0 -0
  49. {mini_arcade_core-1.0.2 → mini_arcade_core-1.1.0}/src/mini_arcade_core/runtime/capture/capture_port.py +0 -0
  50. {mini_arcade_core-1.0.2 → mini_arcade_core-1.1.0}/src/mini_arcade_core/runtime/context.py +0 -0
  51. {mini_arcade_core-1.0.2/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.2 → mini_arcade_core-1.1.0}/src/mini_arcade_core/runtime/file/file_adapter.py +0 -0
  53. {mini_arcade_core-1.0.2 → mini_arcade_core-1.1.0}/src/mini_arcade_core/runtime/file/file_port.py +0 -0
  54. {mini_arcade_core-1.0.2/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.2 → mini_arcade_core-1.1.0}/src/mini_arcade_core/runtime/input/input_adapter.py +0 -0
  56. {mini_arcade_core-1.0.2 → mini_arcade_core-1.1.0}/src/mini_arcade_core/runtime/input/input_port.py +0 -0
  57. {mini_arcade_core-1.0.2 → mini_arcade_core-1.1.0}/src/mini_arcade_core/runtime/input_frame.py +0 -0
  58. {mini_arcade_core-1.0.2/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.2/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.2/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.2 → mini_arcade_core-1.1.0}/src/mini_arcade_core/runtime/window/window_adapter.py +0 -0
  62. {mini_arcade_core-1.0.2 → mini_arcade_core-1.1.0}/src/mini_arcade_core/runtime/window/window_port.py +0 -0
  63. {mini_arcade_core-1.0.2/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.2 → mini_arcade_core-1.1.0}/src/mini_arcade_core/scenes/autoreg.py +0 -0
  65. {mini_arcade_core-1.0.2 → mini_arcade_core-1.1.0}/src/mini_arcade_core/scenes/sim_scene.py +0 -0
  66. {mini_arcade_core-1.0.2/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.2 → mini_arcade_core-1.1.0}/src/mini_arcade_core/scenes/systems/base_system.py +0 -0
  68. {mini_arcade_core-1.0.2 → mini_arcade_core-1.1.0}/src/mini_arcade_core/scenes/systems/system_pipeline.py +0 -0
  69. {mini_arcade_core-1.0.2/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.2 → mini_arcade_core-1.1.0}/src/mini_arcade_core/sim/protocols.py +0 -0
  71. {mini_arcade_core-1.0.2 → mini_arcade_core-1.1.0}/src/mini_arcade_core/sim/runner.py +0 -0
  72. {mini_arcade_core-1.0.2/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.2 → mini_arcade_core-1.1.0}/src/mini_arcade_core/spaces/d2/boundaries2d.py +0 -0
  74. {mini_arcade_core-1.0.2 → mini_arcade_core-1.1.0}/src/mini_arcade_core/spaces/d2/collision2d.py +0 -0
  75. {mini_arcade_core-1.0.2 → mini_arcade_core-1.1.0}/src/mini_arcade_core/spaces/d2/geometry2d.py +0 -0
  76. {mini_arcade_core-1.0.2 → mini_arcade_core-1.1.0}/src/mini_arcade_core/spaces/d2/kinematics2d.py +0 -0
  77. {mini_arcade_core-1.0.2 → mini_arcade_core-1.1.0}/src/mini_arcade_core/spaces/d2/physics2d.py +0 -0
  78. {mini_arcade_core-1.0.2 → mini_arcade_core-1.1.0}/src/mini_arcade_core/ui/menu.py +0 -0
  79. {mini_arcade_core-1.0.2 → mini_arcade_core-1.1.0}/src/mini_arcade_core/utils/__init__.py +0 -0
  80. {mini_arcade_core-1.0.2 → mini_arcade_core-1.1.0}/src/mini_arcade_core/utils/deprecated_decorator.py +0 -0
  81. {mini_arcade_core-1.0.2 → 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.2
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.2"
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" },
@@ -173,11 +173,15 @@ class ChangeSceneCommand(Command):
173
173
 
174
174
  @dataclass(frozen=True)
175
175
  class ToggleDebugOverlayCommand(Command):
176
- """Toggle the debug overlay scene."""
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) -> None:
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)
@@ -17,8 +17,11 @@ from mini_arcade_core.engine.commands import (
17
17
  QuitCommand,
18
18
  ToggleDebugOverlayCommand,
19
19
  )
20
+ from mini_arcade_core.engine.render.context import RenderContext
21
+ from mini_arcade_core.engine.render.frame_packet import FramePacket
20
22
  from mini_arcade_core.engine.render.packet import RenderPacket
21
23
  from mini_arcade_core.engine.render.pipeline import RenderPipeline
24
+ from mini_arcade_core.engine.render.render_service import RenderService
22
25
  from mini_arcade_core.managers.cheats import CheatManager
23
26
  from mini_arcade_core.runtime.audio.audio_adapter import SDLAudioAdapter
24
27
  from mini_arcade_core.runtime.capture.capture_adapter import CaptureAdapter
@@ -188,6 +191,7 @@ class Game:
188
191
  files=LocalFilesAdapter(),
189
192
  capture=CaptureAdapter(self.backend),
190
193
  input=InputAdapter(),
194
+ render=RenderService(),
191
195
  )
192
196
 
193
197
  self.command_queue = CommandQueue()
@@ -304,23 +308,39 @@ class Game:
304
308
  cmd.execute(command_context)
305
309
  timer.mark("cmd_exec_end")
306
310
 
311
+ # ---------------- TO REPLACE WITH RENDERING PIPELINE ----------------
307
312
  timer.mark("render_start")
308
- backend.begin_frame()
309
- timer.mark("begin_frame_done")
310
313
 
311
314
  vp = self.services.window.get_viewport()
315
+
316
+ # gather visible packets
317
+ frame_packets: list[RenderPacket] = []
312
318
  for entry in self.services.scenes.visible_entries():
313
319
  scene = entry.scene
314
320
  packet = packet_cache.get(id(scene))
315
321
  if packet is None:
316
- # bootstrap (first frame visible but not updated)
317
322
  packet = scene.tick(_neutral_input(frame_index, 0.0), 0.0)
318
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
+ )
319
337
 
320
- 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)
321
341
 
322
- timer.mark("draw_done")
323
- backend.end_frame()
342
+ timer.mark("render_done")
343
+ # ---------------- END RENDERING PIPELINE ----------------------------
324
344
  timer.mark("end_frame_done")
325
345
 
326
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
@@ -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) -> None:
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:
@@ -161,7 +161,7 @@ class ScenePort:
161
161
  :rtype: bool
162
162
  """
163
163
 
164
- def remove_scene(self, scene_id: str) -> None:
164
+ def remove_scene(self, scene_id: str):
165
165
  """
166
166
  Remove a scene with the given ID from the stack.
167
167
 
@@ -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.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):
@@ -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()