diff --git a/frameos/src/apps/render/image/app.nim b/frameos/src/apps/render/image/app.nim index 600f5338d..ceeb05042 100644 --- a/frameos/src/apps/render/image/app.nim +++ b/frameos/src/apps/render/image/app.nim @@ -17,6 +17,10 @@ type App* = ref object of AppRoot appConfig*: AppConfig +proc clearTransientInputs(self: App) = + self.appConfig.inputImage = none(Image) + self.appConfig.image = nil + proc render*(self: App, context: ExecutionContext, image: Image) = try: let sourceImage = self.appConfig.image @@ -53,13 +57,19 @@ proc render*(self: App, context: ExecutionContext, image: Image) = scaleAndDrawImage(image, errorImage, self.appConfig.placement) proc run*(self: App, context: ExecutionContext) = - render(self, context, context.image) + try: + render(self, context, context.image) + finally: + self.clearTransientInputs() proc get*(self: App, context: ExecutionContext): Image = - result = if self.appConfig.inputImage.isSome: - self.appConfig.inputImage.get() - elif context.hasImage: - newImage(context.image.width, context.image.height) - else: - newImage(self.frameConfig.renderWidth(), self.frameConfig.renderHeight()) - render(self, context, result) + try: + result = if self.appConfig.inputImage.isSome: + self.appConfig.inputImage.get() + elif context.hasImage: + newImage(context.image.width, context.image.height) + else: + newImage(self.frameConfig.renderWidth(), self.frameConfig.renderHeight()) + render(self, context, result) + finally: + self.clearTransientInputs() diff --git a/frameos/src/apps/render/image/tests/test_app.nim b/frameos/src/apps/render/image/tests/test_app.nim index 08795819c..b7380ec71 100644 --- a/frameos/src/apps/render/image/tests/test_app.nim +++ b/frameos/src/apps/render/image/tests/test_app.nim @@ -45,6 +45,7 @@ suite "render/image app": test "run renders onto context image in place": let source = newImage(1, 1) source.data[0] = rgbx(200, 100, 50, 255) + let input = newImage(3, 2) let config = makeConfig(3, 2) let app = App( frameConfig: config, @@ -58,7 +59,7 @@ suite "render/image app": blendMode: "normal" ) ) - let context = ExecutionContext(image: newImage(3, 2), hasImage: true) + let context = ExecutionContext(image: input, hasImage: true) app.run(context) @@ -66,6 +67,33 @@ suite "render/image app": check sample.r > 0 check sample.g > 0 check sample.b > 0 + check app.appConfig.image.isNil + check app.appConfig.inputImage.isNone + + test "get clears transient source images after returning output": + let source = newImage(1, 1) + source.data[0] = rgbx(50, 200, 100, 255) + let input = newImage(3, 2) + let config = makeConfig(3, 2) + let app = App( + frameConfig: config, + scene: makeScene(config), + appConfig: AppConfig( + inputImage: some(input), + image: source, + placement: "stretch", + offsetX: 0, + offsetY: 0, + blendMode: "normal" + ) + ) + + let output = app.get(ExecutionContext(hasImage: false)) + + check output == input + check pixel(output, 2, 1).g > 0 + check app.appConfig.image.isNil + check app.appConfig.inputImage.isNone test "missing source image is handled by error path without raising": let config = makeConfig(4, 3) diff --git a/frameos/src/frameos/runner.nim b/frameos/src/frameos/runner.nim index ed33d2906..db58eae69 100644 --- a/frameos/src/frameos/runner.nim +++ b/frameos/src/frameos/runner.nim @@ -16,6 +16,7 @@ import frameos/utils/time import frameos/scenes import frameos/boot_guard import frameos/runtime_diagnostics +import frameos/utils/memory import frameos/watchdog import drivers/drivers as drivers @@ -225,7 +226,9 @@ proc startRenderLoop*(self: RunnerThread, maxCycles = -1): Future[void] {.async. self.triggerRenderNext = false # used to debounce render events received while rendering let interval = currentScene.refreshInterval - let (lastRotatedImage, nextSleep) = self.renderSceneImage(exportedScene.get(), currentScene) + var renderResult = self.renderSceneImage(exportedScene.get(), currentScene) + var lastRotatedImage = renderResult[0] + let nextSleep = renderResult[1] reclaimRetiredExportedScenes(currentExportedScenesGeneration(), self.logger) clearBootCrashCount() successfulSceneRenders += 1 @@ -261,6 +264,10 @@ proc startRenderLoop*(self: RunnerThread, maxCycles = -1): Future[void] {.async. except Exception as e: self.logger.log(%*{"event": "render:driver:error", "error": $e.msg, "stacktrace": e.getStackTrace()}) finally: + lastRotatedImage = nil + renderResult[0] = nil + if self.frameConfig.device == "framebuffer": + reclaimRenderMemory() clearNextRenderSeconds() markRuntimeDone() diff --git a/frameos/src/frameos/utils/memory.nim b/frameos/src/frameos/utils/memory.nim new file mode 100644 index 000000000..3af9e1fc4 --- /dev/null +++ b/frameos/src/frameos/utils/memory.nim @@ -0,0 +1,9 @@ +proc reclaimRenderMemory*() = + ## Large image decodes can leave sizeable temporary allocations behind. + ## Collect unreachable Nim objects first, then ask glibc malloc to return + ## free heap pages to the OS on Linux frame targets. + GC_fullCollect() + + when defined(linux): + proc malloc_trim(pad: csize_t): cint {.importc: "malloc_trim", header: "".} + discard malloc_trim(0.csize_t)