Skip to content

Commit aa6c534

Browse files
authored
Shared scene libraries (#202)
* Add shared scene libraries * Remove legacy driver build mode naming
1 parent 12a5a91 commit aa6c534

58 files changed

Lines changed: 1301 additions & 285 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

backend/app/api/frames.py

Lines changed: 15 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -104,7 +104,7 @@
104104
from app.utils.tls import generate_frame_tls_material, parse_certificate_not_valid_after
105105
from app.utils.ssh_authorized_keys import _install_authorized_keys, resolve_authorized_keys_update
106106
from app.tasks.binary_builder import FrameBinaryBuilder
107-
from app.codegen.drivers_nim import frame_driver_build_mode
107+
from app.codegen.drivers_nim import frame_compilation_mode
108108
from app.utils.local_exec import exec_local_command
109109
from app.utils.jwt_tokens import validate_scoped_token
110110
from . import api_with_auth, api_no_auth
@@ -770,7 +770,7 @@ async def api_frame_local_build_zip( # noqa: D401
770770
source_dir = deployer.create_local_source_folder(tmp)
771771

772772
# Apply all frame‑specific code generation (scenes, drivers, …)
773-
await deployer.make_local_modifications(source_dir, driver_build_mode=frame_driver_build_mode(frame))
773+
await deployer.make_local_modifications(source_dir, compilation_mode=frame_compilation_mode(frame))
774774
await copy_custom_fonts_to_local_source_folder(db, source_dir)
775775

776776
# Package → .zip
@@ -833,13 +833,13 @@ async def api_frame_local_c_source_zip(
833833
)
834834

835835
source_dir = deployer.create_local_source_folder(tmp)
836-
driver_build_mode = frame_driver_build_mode(frame)
837-
await deployer.make_local_modifications(source_dir, driver_build_mode=driver_build_mode)
836+
compilation_mode = frame_compilation_mode(frame)
837+
await deployer.make_local_modifications(source_dir, compilation_mode=compilation_mode)
838838
await copy_custom_fonts_to_local_source_folder(db, source_dir)
839839

840840
build_dir = os.path.join(tmp, f"build_{deployer.build_id}")
841841
os.makedirs(build_dir, exist_ok=True)
842-
await deployer.create_local_build_archive(build_dir, source_dir, arch, driver_build_mode=driver_build_mode)
842+
await deployer.create_local_build_archive(build_dir, source_dir, arch, compilation_mode=compilation_mode)
843843

844844
zip_path = os.path.join(tmp, f"frameos_{deployer.build_id}_c_source.zip")
845845
with zipfile.ZipFile(
@@ -922,6 +922,16 @@ async def api_frame_local_binary_zip(
922922
detail=f"Shared driver library missing after build: {driver_library_path}",
923923
)
924924
shutil.copy2(driver_library_path, os.path.join(driver_dir, os.path.basename(driver_library_path)))
925+
if build_result.scene_library_paths:
926+
scene_dir = os.path.join(dist_dir, "scenes")
927+
os.makedirs(scene_dir, exist_ok=True)
928+
for scene_library_path in build_result.scene_library_paths:
929+
if not os.path.isfile(scene_library_path):
930+
raise HTTPException(
931+
status_code=HTTPStatus.INTERNAL_SERVER_ERROR,
932+
detail=f"Shared scene library missing after build: {scene_library_path}",
933+
)
934+
shutil.copy2(scene_library_path, os.path.join(scene_dir, os.path.basename(scene_library_path)))
925935

926936
zip_path = os.path.join(tmp, f"frameos_{deployer.build_id}_binary.zip")
927937
with zipfile.ZipFile(

backend/app/codegen/drivers_nim.py

Lines changed: 20 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -5,33 +5,33 @@
55

66
from app.drivers.drivers import Driver
77

8-
DRIVER_BUILD_MODE_STATIC = "static"
9-
DRIVER_BUILD_MODE_SHARED = "shared"
10-
DRIVER_BUILD_MODE_PRECOMPILED = "precompiled"
11-
DEFAULT_DRIVER_BUILD_MODE = DRIVER_BUILD_MODE_STATIC
12-
VALID_DRIVER_BUILD_MODES = {
13-
DRIVER_BUILD_MODE_STATIC,
14-
DRIVER_BUILD_MODE_SHARED,
15-
DRIVER_BUILD_MODE_PRECOMPILED,
8+
COMPILATION_MODE_STATIC = "static"
9+
COMPILATION_MODE_SHARED = "shared"
10+
COMPILATION_MODE_PRECOMPILED = "precompiled"
11+
DEFAULT_COMPILATION_MODE = COMPILATION_MODE_STATIC
12+
VALID_COMPILATION_MODES = {
13+
COMPILATION_MODE_STATIC,
14+
COMPILATION_MODE_SHARED,
15+
COMPILATION_MODE_PRECOMPILED,
1616
}
1717

1818

19-
def normalize_driver_build_mode(value: str | None) -> str:
20-
normalized = (value or DEFAULT_DRIVER_BUILD_MODE).strip().lower()
21-
if normalized not in VALID_DRIVER_BUILD_MODES:
22-
return DEFAULT_DRIVER_BUILD_MODE
19+
def normalize_compilation_mode(value: str | None) -> str:
20+
normalized = (value or DEFAULT_COMPILATION_MODE).strip().lower()
21+
if normalized not in VALID_COMPILATION_MODES:
22+
return DEFAULT_COMPILATION_MODE
2323
return normalized
2424

2525

26-
def frame_driver_build_mode(frame) -> str:
26+
def frame_compilation_mode(frame) -> str:
2727
rpios_settings = getattr(frame, "rpios", None) or {}
28-
return normalize_driver_build_mode(rpios_settings.get("driverBuildMode"))
28+
return normalize_compilation_mode(rpios_settings.get("compilationMode"))
2929

3030

31-
def driver_build_mode_uses_shared_libraries(value: str | None) -> bool:
32-
return normalize_driver_build_mode(value) in {
33-
DRIVER_BUILD_MODE_SHARED,
34-
DRIVER_BUILD_MODE_PRECOMPILED,
31+
def compilation_mode_uses_shared_libraries(value: str | None) -> bool:
32+
return normalize_compilation_mode(value) in {
33+
COMPILATION_MODE_SHARED,
34+
COMPILATION_MODE_PRECOMPILED,
3535
}
3636

3737

@@ -241,9 +241,9 @@ def driver_library_context_helpers_nim() -> str:
241241

242242
def write_drivers_nim(
243243
drivers: dict[str, Driver],
244-
driver_build_mode: str = DEFAULT_DRIVER_BUILD_MODE,
244+
compilation_mode: str = DEFAULT_COMPILATION_MODE,
245245
) -> str:
246-
if driver_build_mode_uses_shared_libraries(driver_build_mode):
246+
if compilation_mode_uses_shared_libraries(compilation_mode):
247247
return write_shared_drivers_nim(drivers)
248248
return write_static_drivers_nim(drivers)
249249

backend/app/codegen/scene_nim.py

Lines changed: 180 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,10 @@
77

88
from app.models.frame import Frame
99
from app.models.apps import get_local_frame_apps, get_local_app_path, get_scene_app_id
10+
from app.codegen.drivers_nim import (
11+
DEFAULT_COMPILATION_MODE,
12+
compilation_mode_uses_shared_libraries,
13+
)
1014
from app.codegen.utils import sanitize_nim_string, natural_keys
1115
from app.utils.js_apps import find_js_app_source_key
1216

@@ -36,6 +40,50 @@ def write_scene_nim(frame: Frame, scene: dict) -> str:
3640
return SceneWriter(frame, scene).write_scene_nim()
3741

3842

43+
def compiled_frame_scenes(frame: Frame) -> list[dict]:
44+
scenes = getattr(frame, "scenes", None) or []
45+
compiled_scenes = []
46+
for scene in scenes:
47+
if not isinstance(scene, dict):
48+
continue
49+
settings = scene.get("settings") or {}
50+
if settings.get("execution", "compiled") != "interpreted":
51+
compiled_scenes.append(scene)
52+
return compiled_scenes
53+
54+
55+
def scene_registry_id(scene: dict) -> str:
56+
return re.sub(r"[^a-zA-Z0-9\-\_]", "_", scene.get("id", "default"))
57+
58+
59+
def scene_module_suffix(scene: dict) -> str:
60+
return re.sub(r"\W+", "", scene_registry_id(scene))
61+
62+
63+
def scene_module_filename(scene: dict) -> str:
64+
return f"scene_{scene_module_suffix(scene)}.nim"
65+
66+
67+
def scene_library_filename(scene: dict) -> str:
68+
return f"scene_{scene_module_suffix(scene)}.so"
69+
70+
71+
def write_scene_library_nim(scene: dict) -> str:
72+
scene_module = f"scene_{scene_module_suffix(scene)}"
73+
return f"""# This file is autogenerated
74+
75+
import scenes/{scene_module} as sceneModule
76+
import frameos/channels
77+
import frameos/driver_abi
78+
79+
proc frameos_scene_init*(logHook: HostLogProc, sendEventHook: HostSendEventProc) {{.cdecl, exportc, dynlib.}} =
80+
setSharedHostCallbacks(logHook, sendEventHook)
81+
82+
proc frameos_scene_export*(): pointer {{.cdecl, exportc, dynlib.}} =
83+
result = cast[pointer](sceneModule.exportedScene)
84+
"""
85+
86+
3987
def field_type_to_nim_type(field_type: str, required: bool = True) -> str:
4088
match field_type:
4189
case 'select':
@@ -1633,47 +1681,46 @@ def wrap_with_cache(self, node_id: str, value_list: list[str], data: dict):
16331681
return value_list
16341682

16351683

1636-
def write_scenes_nim(frame: Frame) -> str:
1684+
def _scene_registry_rows(frame: Frame) -> tuple[list[str], list[str], dict | None]:
16371685
rows = []
1638-
imports = []
16391686
sceneOptionTuples = []
16401687
default_scene = None
1641-
for scene in frame.scenes:
1642-
execution = scene.get("settings", {}).get("execution", "compiled")
1643-
if execution == "interpreted":
1644-
continue
1688+
for scene in compiled_frame_scenes(frame):
16451689
if scene.get("default", False):
16461690
default_scene = scene
16471691

1648-
scene_id = scene.get("id", "default")
1649-
scene_id = re.sub(r"[^a-zA-Z0-9\-\_]", "_", scene_id)
1650-
scene_id_import = re.sub(r"\W+", "", scene_id)
1651-
imports.append(
1652-
f"import scenes/scene_{scene_id_import} as scene_{scene_id_import}"
1653-
)
1692+
scene_id = scene_registry_id(scene)
16541693
rows.append(
1655-
f' result["{scene_id}".SceneId] = scene_{scene_id_import}.exportedScene'
1694+
f' result["{scene_id}".SceneId] = scene_{scene_module_suffix(scene)}.exportedScene'
16561695
)
16571696
sceneOptionTuples.append(
1658-
f" (\"{scene_id}\".SceneId, \"{scene.get('name', 'Default')}\"),"
1697+
f' ("{scene_id}".SceneId, "{sanitize_nim_string(scene.get("name", "Default"))}"),'
16591698
)
1699+
return rows, sceneOptionTuples, default_scene
1700+
16601701

1702+
def _default_scene_line(default_scene: dict | None) -> str:
16611703
default_scene_id = (
16621704
default_scene.get("id", None) if default_scene is not None else None
16631705
)
16641706
if default_scene_id is None:
1665-
default_scene_line = "let defaultSceneId* = none(SceneId)"
1666-
else:
1667-
default_scene_id = re.sub(r"[^a-zA-Z0-9\-\_]", "_", default_scene_id)
1668-
default_scene_line = f'let defaultSceneId* = some("{default_scene_id}".SceneId)'
1707+
return "let defaultSceneId* = none(SceneId)"
1708+
return f'let defaultSceneId* = some("{scene_registry_id(default_scene)}".SceneId)'
1709+
16691710

1711+
def write_static_scenes_nim(frame: Frame) -> str:
1712+
rows, sceneOptionTuples, default_scene = _scene_registry_rows(frame)
1713+
imports = [
1714+
f"import scenes/scene_{scene_module_suffix(scene)} as scene_{scene_module_suffix(scene)}"
1715+
for scene in compiled_frame_scenes(frame)
1716+
]
16701717
newline = "\n"
16711718
scenes_source = f"""
16721719
import frameos/types
16731720
import tables, options
16741721
{newline.join(sorted(imports))}
16751722
1676-
{default_scene_line}
1723+
{_default_scene_line(default_scene)}
16771724
16781725
const sceneOptions*: array[{len(sceneOptionTuples)}, tuple[id: SceneId, name: string]] = [
16791726
{newline.join(sorted(sceneOptionTuples))}
@@ -1685,3 +1732,117 @@ def write_scenes_nim(frame: Frame) -> str:
16851732
"""
16861733

16871734
return scenes_source
1735+
1736+
1737+
def write_shared_scenes_nim(frame: Frame) -> str:
1738+
compiled_scenes = compiled_frame_scenes(frame)
1739+
sceneOptionTuples = [
1740+
f' ("{scene_registry_id(scene)}".SceneId, "{sanitize_nim_string(scene.get("name", "Default"))}"),'
1741+
for scene in compiled_scenes
1742+
]
1743+
default_scene = next((scene for scene in compiled_scenes if scene.get("default", False)), None)
1744+
specs = [
1745+
"SceneSpec("
1746+
f'id: "{scene_registry_id(scene)}".SceneId, '
1747+
f'name: "{sanitize_nim_string(scene.get("name", "Default"))}", '
1748+
f'libraryName: "{scene_library_filename(scene)}"'
1749+
")"
1750+
for scene in compiled_scenes
1751+
]
1752+
1753+
newline = "\n"
1754+
spec_lines = ("," + newline + " ").join(specs)
1755+
if spec_lines:
1756+
spec_lines = newline + " " + spec_lines + newline
1757+
1758+
scenes_source = f"""
1759+
import std/[dynlib, json, options, os, tables]
1760+
import frameos/types
1761+
import frameos/channels as hostChannels
1762+
import frameos/driver_abi
1763+
1764+
type
1765+
SceneSpec = object
1766+
id: SceneId
1767+
name: string
1768+
libraryName: string
1769+
1770+
LoadedSceneLibrary = object
1771+
spec: SceneSpec
1772+
library: LibHandle
1773+
exportedScene: ExportedScene
1774+
1775+
SceneInitProc = proc(logHook: HostLogProc, sendEventHook: HostSendEventProc) {{.cdecl.}}
1776+
SceneExportProc = proc(): pointer {{.cdecl.}}
1777+
1778+
let sceneSpecs: seq[SceneSpec] = @[{spec_lines}]
1779+
var loadedSceneLibraries: seq[LoadedSceneLibrary] = @[]
1780+
1781+
{_default_scene_line(default_scene)}
1782+
1783+
const sceneOptions*: array[{len(sceneOptionTuples)}, tuple[id: SceneId, name: string]] = [
1784+
{newline.join(sorted(sceneOptionTuples))}
1785+
]
1786+
1787+
proc hostLog(event: JsonNode) {{.cdecl, gcsafe.}} =
1788+
hostChannels.log(event)
1789+
1790+
proc hostSendEvent(scene: Option[SceneId], event: string, payload: JsonNode) {{.cdecl, gcsafe.}} =
1791+
hostChannels.sendEvent(scene, event, payload)
1792+
1793+
proc sceneLibraryPath(spec: SceneSpec): string =
1794+
getAppDir() / "scenes" / spec.libraryName
1795+
1796+
proc loadRequiredSymbol[T](library: LibHandle, sceneId: SceneId, symbol: string): T =
1797+
let address = symAddr(library, symbol)
1798+
if address.isNil:
1799+
hostChannels.log(%*{{"event": "scene:shared:error", "sceneId": sceneId.string,
1800+
"error": "Missing symbol", "symbol": symbol}})
1801+
return nil
1802+
cast[T](address)
1803+
1804+
proc loadSharedScene(spec: SceneSpec): Option[ExportedScene] =
1805+
let path = sceneLibraryPath(spec)
1806+
let library = loadLib(path)
1807+
if library.isNil:
1808+
hostChannels.log(%*{{"event": "scene:shared:error", "sceneId": spec.id.string,
1809+
"error": "Unable to load scene library", "path": path}})
1810+
return none(ExportedScene)
1811+
1812+
let initProc = loadRequiredSymbol[SceneInitProc](library, spec.id, "frameos_scene_init")
1813+
if initProc.isNil:
1814+
unloadLib(library)
1815+
return none(ExportedScene)
1816+
initProc(hostLog, hostSendEvent)
1817+
1818+
let exportProc = loadRequiredSymbol[SceneExportProc](library, spec.id, "frameos_scene_export")
1819+
if exportProc.isNil:
1820+
unloadLib(library)
1821+
return none(ExportedScene)
1822+
1823+
let exportedScene = cast[ExportedScene](exportProc())
1824+
if exportedScene.isNil:
1825+
hostChannels.log(%*{{"event": "scene:shared:error", "sceneId": spec.id.string,
1826+
"error": "Scene library returned nil export", "path": path}})
1827+
unloadLib(library)
1828+
return none(ExportedScene)
1829+
1830+
loadedSceneLibraries.add(LoadedSceneLibrary(spec: spec, library: library, exportedScene: exportedScene))
1831+
hostChannels.log(%*{{"event": "scene:shared", "sceneId": spec.id.string, "path": path, "loaded": true}})
1832+
return some(exportedScene)
1833+
1834+
proc getExportedScenes*(): Table[SceneId, ExportedScene] =
1835+
result = initTable[SceneId, ExportedScene]()
1836+
for spec in sceneSpecs:
1837+
let exportedScene = loadSharedScene(spec)
1838+
if exportedScene.isSome:
1839+
result[spec.id] = exportedScene.get()
1840+
"""
1841+
1842+
return scenes_source
1843+
1844+
1845+
def write_scenes_nim(frame: Frame, compilation_mode: str = DEFAULT_COMPILATION_MODE) -> str:
1846+
if compilation_mode_uses_shared_libraries(compilation_mode):
1847+
return write_shared_scenes_nim(frame)
1848+
return write_static_scenes_nim(frame)

0 commit comments

Comments
 (0)