Linked repository source
examples/cavern_lab/live/project/bake/bake_shell.gd
This read-only view comes from the file in the Gododin repository. It retains its design, example or evidence context.
Download the original file · Return to the guide
@tool
extends RefCounted
# The Phase 14 bake shell (bake/README.md), scripted equivalent for evidence
# runs and the reusable core of the editor panel. It saves the request,
# computes the digest, and supervises one bounded headless child. It never
# compiles, links, or stages a native artifact itself. One outstanding bake
# per request resource: launching a new bake supersedes the outstanding child,
# and cancel terminates the child without touching the destination. The shell
# verifies publication by loading the destination and comparing its recorded
# request digest.
const DIGEST := preload("res://bake/bake_digest.gd")
var godot_binary: String
var timeout_seconds := 180.0
var last_error := ""
# Set when cancel() terminated the outstanding child, so a poller can name the
# disposition honestly instead of reporting a deliberate cancel as a failure.
var cancelled := false
var _pid := 0
var _exit_code := -1
var _finalized := ""
var _output_path := ""
var _digest := ""
func _init() -> void:
godot_binary = OS.get_executable_path()
func digest_for(request) -> String:
var mask_image: Image = request.mask
if mask_image == null:
return ""
return DIGEST.digest(
mask_image.get_data(), mask_image.get_width(), mask_image.get_height(),
int(request.smoothing_steps), int(request.minimum_region_size))
func save_request(request, request_path: String) -> String:
var digest := digest_for(request)
if digest.is_empty():
last_error = "request has no digestable mask"
return ""
# ResourceSaver does not create parent directories, and a redirected HOME
# starts without any user-space path.
DirAccess.make_dir_recursive_absolute(ProjectSettings.globalize_path(request_path.get_base_dir()))
var save_error := ResourceSaver.save(request, request_path)
if save_error != OK:
last_error = "request save error %d" % save_error
return ""
return digest
func outstanding() -> bool:
return _pid != 0 and OS.is_process_running(_pid)
func launch(request, request_path: String, output_path: String) -> bool:
_supersede_outstanding()
# The shell saves the request immediately before spawning, so the child
# always digests exactly the bytes this launch supervises.
var digest := save_request(request, request_path)
if digest.is_empty():
return false
var arguments := PackedStringArray([
"--headless",
"--path", ProjectSettings.globalize_path("res://"),
"--script", "res://bake/bake_process.gd",
"--", request_path, output_path, digest,
])
_pid = OS.create_process(godot_binary, arguments)
if _pid == 0:
last_error = "child spawn failed"
return false
_output_path = output_path
_digest = digest
cancelled = false
_exit_code = -1
_finalized = ""
last_error = ""
return true
func cancel() -> bool:
if not outstanding():
return false
# The child may have exited since the last poll; killing an exited
# process makes the engine print a child-tracking error.
if OS.is_process_running(_pid):
OS.kill(_pid)
_wait_exit(2.0)
# Capture the terminated child's exit code before dropping the handle:
# poll() must verify against a nonzero code, never against a stale one.
_exit_code = OS.get_process_exit_code(_pid)
_pid = 0
cancelled = true
return true
# Non-blocking completion check for interactive callers: empty while the child
# runs, then the disposition ("published"/"failed") exactly once. Verification
# runs on the first completion and the verdict is cached, so the frame loop can
# keep rendering while the supervised bake proceeds.
func poll() -> String:
if not _finalized.is_empty():
return _finalized
if outstanding():
return ""
if _pid != 0:
_exit_code = OS.get_process_exit_code(_pid)
_pid = 0
_finalized = _verify_publication(_exit_code)
return _finalized
func wait_until_finished(deadline_seconds := -1.0) -> String:
if deadline_seconds < 0.0:
deadline_seconds = timeout_seconds
var deadline := Time.get_ticks_usec() + int(deadline_seconds * 1_000_000.0)
while outstanding():
if Time.get_ticks_usec() > deadline:
last_error = "child timed out"
OS.kill(_pid)
_wait_exit(2.0)
_exit_code = OS.get_process_exit_code(_pid)
_pid = 0
return _verify_publication(_exit_code)
OS.delay_msec(10)
if _pid != 0:
_exit_code = OS.get_process_exit_code(_pid)
_pid = 0
return _verify_publication(_exit_code)
func _supersede_outstanding() -> void:
if _pid == 0:
return
if OS.is_process_running(_pid):
OS.kill(_pid)
_wait_exit(2.0)
_pid = 0
func _wait_exit(grace_seconds: float) -> void:
var deadline := Time.get_ticks_usec() + int(grace_seconds * 1_000_000.0)
while OS.is_process_running(_pid) and Time.get_ticks_usec() < deadline:
OS.delay_msec(5)
func _verify_publication(exit_code: int) -> String:
if exit_code != 0:
last_error = "child exit code %d" % exit_code
return "failed"
if not FileAccess.file_exists(_output_path):
last_error = "destination was not published"
return "failed"
var data = load(_output_path)
if data == null:
last_error = "destination is not a loadable resource"
return "failed"
if String(data.request_digest) != _digest:
last_error = "destination digest differs from the request"
return "failed"
return "published"