Skip to content

Commit bd631ee

Browse files
committed
Add shared scene bundle mode and precompiled fallback
1 parent 9a7d2b3 commit bd631ee

16 files changed

Lines changed: 449 additions & 37 deletions

backend/app/codegen/drivers_nim.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,11 +7,13 @@
77

88
COMPILATION_MODE_STATIC = "static"
99
COMPILATION_MODE_SHARED = "shared"
10+
COMPILATION_MODE_SHARED_SCENES = "shared-scenes"
1011
COMPILATION_MODE_PRECOMPILED = "precompiled"
1112
DEFAULT_COMPILATION_MODE = COMPILATION_MODE_PRECOMPILED
1213
VALID_COMPILATION_MODES = {
1314
COMPILATION_MODE_STATIC,
1415
COMPILATION_MODE_SHARED,
16+
COMPILATION_MODE_SHARED_SCENES,
1517
COMPILATION_MODE_PRECOMPILED,
1618
}
1719

@@ -31,6 +33,7 @@ def frame_compilation_mode(frame) -> str:
3133
def compilation_mode_uses_shared_libraries(value: str | None) -> bool:
3234
return normalize_compilation_mode(value) in {
3335
COMPILATION_MODE_SHARED,
36+
COMPILATION_MODE_SHARED_SCENES,
3437
COMPILATION_MODE_PRECOMPILED,
3538
}
3639

backend/app/codegen/scene_nim.py

Lines changed: 151 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
from app.models.apps import get_local_frame_apps, get_local_app_path, get_scene_app_id
1010
from app.codegen.drivers_nim import (
1111
DEFAULT_COMPILATION_MODE,
12+
COMPILATION_MODE_SHARED_SCENES,
1213
compilation_mode_uses_shared_libraries,
1314
)
1415
from app.codegen.utils import sanitize_nim_string, natural_keys
@@ -68,6 +69,22 @@ def scene_library_filename(scene: dict) -> str:
6869
return f"scene_{scene_module_suffix(scene)}.so"
6970

7071

72+
def scene_bundle_library_filename() -> str:
73+
return "scenes.so"
74+
75+
76+
def _scene_symbol_suffix(scene: dict) -> str:
77+
return scene_module_suffix(scene).replace(".", "_").replace("-", "_")
78+
79+
80+
def _scene_bundle_init_symbol(scene: dict) -> str:
81+
return f"frameos_scene_init_{_scene_symbol_suffix(scene)}"
82+
83+
84+
def _scene_bundle_export_symbol(scene: dict) -> str:
85+
return f"frameos_scene_export_{_scene_symbol_suffix(scene)}"
86+
87+
7188
def write_scene_library_nim(scene: dict) -> str:
7289
scene_module = f"scene_{scene_module_suffix(scene)}"
7390
return f"""# This file is autogenerated
@@ -1842,7 +1859,141 @@ def write_shared_scenes_nim(frame: Frame) -> str:
18421859
return scenes_source
18431860

18441861

1862+
def write_shared_scenes_bundle_nim(frame: Frame) -> str:
1863+
compiled_scenes = compiled_frame_scenes(frame)
1864+
scene_specs = [
1865+
"SceneBundleSpec("
1866+
f'id: "{scene_registry_id(scene)}".SceneId, '
1867+
f'name: "{sanitize_nim_string(scene.get("name", "Default"))}", '
1868+
f'libraryName: "{scene_bundle_library_filename()}", '
1869+
f'initSymbol: "{_scene_bundle_init_symbol(scene)}", '
1870+
f'exportSymbol: "{_scene_bundle_export_symbol(scene)}"'
1871+
")"
1872+
for scene in compiled_scenes
1873+
]
1874+
default_scene = next((scene for scene in compiled_scenes if scene.get("default", False)), None)
1875+
sceneOptionTuples = [
1876+
f' ("{scene_registry_id(scene)}".SceneId, "{sanitize_nim_string(scene.get("name", "Default"))}"),'
1877+
for scene in compiled_scenes
1878+
]
1879+
spec_lines = ("," + "\n" + " ").join(scene_specs)
1880+
if spec_lines:
1881+
spec_lines = "\n " + spec_lines + "\n"
1882+
newline = "\n"
1883+
1884+
bundle_imports = "\n".join(
1885+
f'import scenes.scene_{scene_module_suffix(scene)} as scene_{scene_module_suffix(scene)}'
1886+
for scene in compiled_scenes
1887+
)
1888+
if not bundle_imports:
1889+
bundle_imports = "# no compiled scenes"
1890+
1891+
wrapper_exports = ""
1892+
for scene in compiled_scenes:
1893+
wrapper_exports += f"""
1894+
proc {_scene_bundle_init_symbol(scene)}*(logHook: HostLogProc, sendEventHook: HostSendEventProc) {{.cdecl, exportc, dynlib.}} =
1895+
scene_{scene_module_suffix(scene)}.frameos_scene_init(logHook, sendEventHook)
1896+
1897+
proc {_scene_bundle_export_symbol(scene)}*(): pointer {{.cdecl, exportc, dynlib.}} =
1898+
result = cast[pointer](scene_{scene_module_suffix(scene)}.exportedScene)
1899+
"""
1900+
1901+
scenes_source = f"""
1902+
import std/[dynlib, json, options, os, tables]
1903+
import frameos/types
1904+
import frameos/channels as hostChannels
1905+
import frameos/driver_abi
1906+
1907+
{bundle_imports}
1908+
1909+
type
1910+
SceneBundleSpec = object
1911+
id: SceneId
1912+
name: string
1913+
libraryName: string
1914+
initSymbol: string
1915+
exportSymbol: string
1916+
LoadedSceneLibrary = object
1917+
spec: SceneBundleSpec
1918+
library: LibHandle
1919+
exportedScene: ExportedScene
1920+
1921+
const sceneSpecs*: seq[SceneBundleSpec] = @[{spec_lines}]
1922+
1923+
{_default_scene_line(default_scene)}
1924+
1925+
const sceneOptions*: array[{len(sceneOptionTuples)}, tuple[id: SceneId, name: string]] = [
1926+
{newline.join(sorted(sceneOptionTuples))}
1927+
]
1928+
var loadedSceneLibraries: seq[LoadedSceneLibrary] = @[]
1929+
1930+
type
1931+
SceneInitProc = proc(logHook: HostLogProc, sendEventHook: HostSendEventProc) {{.cdecl.}}
1932+
SceneExportProc = proc(): pointer {{.cdecl.}}
1933+
1934+
proc hostLog(event: JsonNode) {{.cdecl, gcsafe.}} =
1935+
hostChannels.log(event)
1936+
1937+
proc hostSendEvent(scene: Option[SceneId], event: string, payload: JsonNode) {{.cdecl, gcsafe.}} =
1938+
hostChannels.sendEvent(scene, event, payload)
1939+
1940+
proc sceneLibraryPath(spec: SceneBundleSpec): string =
1941+
getAppDir() / "scenes" / spec.libraryName
1942+
1943+
proc loadRequiredSymbol[T](library: LibHandle, sceneId: SceneId, symbol: string): T =
1944+
let address = symAddr(library, symbol)
1945+
if address.isNil:
1946+
hostChannels.log(%*{{"event": "scene:shared:error", "sceneId": sceneId.string,
1947+
"error": "Missing symbol", "symbol": symbol}})
1948+
return nil
1949+
cast[T](address)
1950+
1951+
proc loadSharedScene(spec: SceneBundleSpec): Option[ExportedScene] =
1952+
let path = sceneLibraryPath(spec)
1953+
let library = loadLib(path)
1954+
if library.isNil:
1955+
hostChannels.log(%*{{"event": "scene:shared:error", "sceneId": spec.id.string,
1956+
"error": "Unable to load scene library", "path": path}})
1957+
return none(ExportedScene)
1958+
1959+
let initProc = loadRequiredSymbol[SceneInitProc](library, spec.id, spec.initSymbol)
1960+
if initProc.isNil:
1961+
unloadLib(library)
1962+
return none(ExportedScene)
1963+
initProc(hostLog, hostSendEvent)
1964+
1965+
let exportProc = loadRequiredSymbol[SceneExportProc](library, spec.id, spec.exportSymbol)
1966+
if exportProc.isNil:
1967+
unloadLib(library)
1968+
return none(ExportedScene)
1969+
1970+
let exportedScene = cast[ExportedScene](exportProc())
1971+
if exportedScene.isNil:
1972+
hostChannels.log(%*{{"event": "scene:shared:error", "sceneId": spec.id.string,
1973+
"error": "Scene library returned nil export", "path": path}})
1974+
unloadLib(library)
1975+
return none(ExportedScene)
1976+
1977+
loadedSceneLibraries.add(LoadedSceneLibrary(spec: spec, library: library, exportedScene: exportedScene))
1978+
hostChannels.log(%*{{"event": "scene:shared", "sceneId": spec.id.string, "path": path, "loaded": true}})
1979+
return some(exportedScene)
1980+
1981+
proc getExportedScenes*(): Table[SceneId, ExportedScene] =
1982+
result = initTable[SceneId, ExportedScene]()
1983+
for spec in sceneSpecs:
1984+
let exportedScene = loadSharedScene(spec)
1985+
if exportedScene.isSome:
1986+
result[spec.id] = exportedScene.get()
1987+
1988+
{wrapper_exports}
1989+
"""
1990+
1991+
return scenes_source
1992+
1993+
18451994
def write_scenes_nim(frame: Frame, compilation_mode: str = DEFAULT_COMPILATION_MODE) -> str:
18461995
if compilation_mode_uses_shared_libraries(compilation_mode):
1996+
if compilation_mode == COMPILATION_MODE_SHARED_SCENES:
1997+
return write_shared_scenes_bundle_nim(frame)
18471998
return write_shared_scenes_nim(frame)
18481999
return write_static_scenes_nim(frame)

backend/app/tasks/_frame_deployer.py

Lines changed: 60 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@
2929
from app.codegen.drivers_nim import (
3030
DEFAULT_COMPILATION_MODE,
3131
COMPILATION_MODE_STATIC,
32+
COMPILATION_MODE_SHARED_SCENES,
3233
compiled_drivers,
3334
compilation_mode_uses_shared_libraries,
3435
driver_library_filename,
@@ -38,6 +39,7 @@
3839
)
3940
from app.codegen.scene_nim import (
4041
compiled_frame_scenes,
42+
scene_bundle_library_filename,
4143
scene_library_filename,
4244
scene_module_filename,
4345
scene_module_suffix,
@@ -230,9 +232,14 @@ def scene_library_paths(
230232
) -> list[str]:
231233
if not compilation_mode_uses_shared_libraries(compilation_mode):
232234
return []
235+
compiled_scenes = compiled_frame_scenes(frame)
236+
if not compiled_scenes:
237+
return []
238+
if compilation_mode == COMPILATION_MODE_SHARED_SCENES:
239+
return [os.path.join(build_dir, "scenes", scene_bundle_library_filename())]
233240
return [
234241
os.path.join(build_dir, "scenes", scene_module_suffix(scene), scene_library_filename(scene))
235-
for scene in compiled_frame_scenes(frame)
242+
for scene in compiled_scenes
236243
]
237244

238245
@staticmethod
@@ -242,6 +249,10 @@ def scene_library_names(
242249
) -> list[str]:
243250
if not compilation_mode_uses_shared_libraries(compilation_mode):
244251
return []
252+
if not compiled_frame_scenes(frame):
253+
return []
254+
if compilation_mode == COMPILATION_MODE_SHARED_SCENES:
255+
return [scene_bundle_library_filename()]
245256
return [scene_library_filename(scene) for scene in compiled_frame_scenes(frame)]
246257

247258
async def _upload_frame_json(self, path: str) -> None:
@@ -438,6 +449,9 @@ async def make_local_modifications(
438449
for scene in compiled_frame_scenes(frame):
439450
with open(os.path.join(shared_scene_dir, scene_module_filename(scene)), "w") as sf:
440451
sf.write(write_scene_library_nim(scene))
452+
if compilation_mode == COMPILATION_MODE_SHARED_SCENES:
453+
with open(os.path.join(source_dir, "src", "scenes", "scenes_bundle.nim"), "w") as bf:
454+
bf.write(write_scenes_nim(frame, compilation_mode=COMPILATION_MODE_SHARED_SCENES))
441455

442456
with open(os.path.join(source_dir, "src", "scenes", "scenes.nim"), "w") as f:
443457
source = write_scenes_nim(frame, compilation_mode=compilation_mode)
@@ -828,6 +842,7 @@ async def create_local_build_archive(
828842
)
829843

830844
if compilation_mode_uses_shared_libraries(compilation_mode):
845+
compiled_scenes = compiled_frame_scenes(frame)
831846
for driver in compiled_drivers(drivers):
832847
driver_dir = os.path.join(build_dir, "drivers", driver.name)
833848
os.makedirs(driver_dir, exist_ok=True)
@@ -866,23 +881,57 @@ async def create_local_build_archive(
866881
)
867882
driver_make_dirs.append(os.path.join("drivers", driver.name))
868883

869-
for scene in compiled_frame_scenes(frame):
870-
scene_dir_name = scene_module_suffix(scene)
871-
scene_dir = os.path.join(build_dir, "scenes", scene_dir_name)
884+
if compilation_mode == COMPILATION_MODE_SHARED:
885+
for scene in compiled_scenes:
886+
scene_dir_name = scene_module_suffix(scene)
887+
scene_dir = os.path.join(build_dir, "scenes", scene_dir_name)
888+
os.makedirs(scene_dir, exist_ok=True)
889+
output_name = scene_library_filename(scene)
890+
await self.log("stdout", f"🔥 Generating C sources for scene {scene.get('id', 'default')}.")
891+
scene_cmd = (
892+
f"cd {source_dir} && {nim_path} compile --app:lib --os:linux --cpu:{cpu} "
893+
f"--define:frameosSharedLibrary {' '.join(SHARED_LIBRARY_NIM_FLAGS)} "
894+
f"--compileOnly --genScript --nimcache:{scene_dir} --out:{output_name} "
895+
f"{debug_options} src/scenes/shared/{scene_module_filename(scene)} 2>&1"
896+
)
897+
scene_status, scene_out, scene_err = await exec_local_command(db, redis, frame, scene_cmd)
898+
if scene_status != 0:
899+
raise Exception(
900+
f"Failed to generate scene library sources for {scene.get('id', 'default')}: "
901+
f"{scene_err or scene_out or 'see logs'}"
902+
)
903+
shutil.copy(nimbase_path, os.path.join(scene_dir, "nimbase.h"))
904+
905+
scene_script_path = self._find_compile_script(scene_dir)
906+
scene_linker_flags, scene_compiler_flags = self._extract_compile_flags(
907+
scene_script_path, output_name
908+
)
909+
scene_linker_flags = self._dedupe_preserve_order(
910+
scene_linker_flags + ["../../quickjs/libquickjs.a"]
911+
)
912+
self._write_driver_makefile(
913+
makefile_path=os.path.join(scene_dir, "Makefile"),
914+
output_name=output_name,
915+
linker_flags=scene_linker_flags,
916+
compiler_flags=scene_compiler_flags,
917+
library_kind="scene",
918+
)
919+
scene_make_dirs.append(os.path.join("scenes", scene_dir_name))
920+
elif compilation_mode == COMPILATION_MODE_SHARED_SCENES and compiled_scenes:
921+
scene_dir = os.path.join(build_dir, "scenes")
872922
os.makedirs(scene_dir, exist_ok=True)
873-
output_name = scene_library_filename(scene)
874-
await self.log("stdout", f"🔥 Generating C sources for scene {scene.get('id', 'default')}.")
923+
output_name = scene_bundle_library_filename()
924+
await self.log("stdout", "🔥 Generating C sources for bundled shared scenes.")
875925
scene_cmd = (
876926
f"cd {source_dir} && {nim_path} compile --app:lib --os:linux --cpu:{cpu} "
877927
f"--define:frameosSharedLibrary {' '.join(SHARED_LIBRARY_NIM_FLAGS)} "
878928
f"--compileOnly --genScript --nimcache:{scene_dir} --out:{output_name} "
879-
f"{debug_options} src/scenes/shared/{scene_module_filename(scene)} 2>&1"
929+
f"{debug_options} src/scenes/scenes_bundle.nim 2>&1"
880930
)
881931
scene_status, scene_out, scene_err = await exec_local_command(db, redis, frame, scene_cmd)
882932
if scene_status != 0:
883933
raise Exception(
884-
f"Failed to generate scene library sources for {scene.get('id', 'default')}: "
885-
f"{scene_err or scene_out or 'see logs'}"
934+
f"Failed to generate bundled scene library sources: {scene_err or scene_out or 'see logs'}"
886935
)
887936
shutil.copy(nimbase_path, os.path.join(scene_dir, "nimbase.h"))
888937

@@ -891,7 +940,7 @@ async def create_local_build_archive(
891940
scene_script_path, output_name
892941
)
893942
scene_linker_flags = self._dedupe_preserve_order(
894-
scene_linker_flags + ["../../quickjs/libquickjs.a"]
943+
scene_linker_flags + ["../quickjs/libquickjs.a"]
895944
)
896945
self._write_driver_makefile(
897946
makefile_path=os.path.join(scene_dir, "Makefile"),
@@ -900,7 +949,7 @@ async def create_local_build_archive(
900949
compiler_flags=scene_compiler_flags,
901950
library_kind="scene",
902951
)
903-
scene_make_dirs.append(os.path.join("scenes", scene_dir_name))
952+
scene_make_dirs.append("scenes")
904953

905954
self._write_c_makefile(
906955
makefile_path=os.path.join(build_dir, "Makefile"),

backend/app/tasks/binary_builder.py

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
from app.codegen.drivers_nim import (
1717
COMPILATION_MODE_PRECOMPILED,
1818
COMPILATION_MODE_STATIC,
19+
COMPILATION_MODE_SHARED_SCENES,
1920
frame_compilation_mode,
2021
normalize_compilation_mode,
2122
)
@@ -211,7 +212,9 @@ async def plan_build(
211212
else:
212213
will_attempt_precompiled = True
213214
if not will_attempt_precompiled:
214-
resolved_compilation_mode = COMPILATION_MODE_STATIC
215+
resolved_compilation_mode = (
216+
COMPILATION_MODE_SHARED_SCENES if compiled_scene_count > 0 else COMPILATION_MODE_STATIC
217+
)
215218

216219
build_host = get_build_host_config(self.db)
217220
cross_compile_supported = can_cross_compile_target(target.arch)

backend/app/tasks/frame_deploy_workflow.py

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -495,14 +495,19 @@ async def _plan_full(self, *, frame_dict: dict[str, Any], previous_frameos_versi
495495
if binary_plan.will_attempt_precompiled:
496496
notes.append("Precompiled FrameOS release will be used because all scenes are interpreted.")
497497
else:
498+
fallback = "single executable"
499+
if binary_plan.compilation_mode == "shared-scenes":
500+
fallback = "compiled scenes library"
501+
elif binary_plan.compilation_mode == "shared":
502+
fallback = "shared libraries"
498503
notes.append(
499504
"Precompiled FrameOS release will be skipped"
500505
+ (
501506
f": {binary_plan.precompiled_skip_reason}."
502507
if binary_plan.precompiled_skip_reason
503508
else "."
504509
)
505-
+ " Falling back to single executable."
510+
+ f" Falling back to {fallback}."
506511
)
507512
if low_memory and not binary_plan.will_attempt_precompiled:
508513
notes.append("Device is low memory; on-device build path will stop FrameOS before compilation.")

0 commit comments

Comments
 (0)