Skip to content

Fix main scene orphan leaks and tune parallax decor with tests#550

Merged
ikostan merged 32 commits intomainfrom
orphan-node-leak-from-placeholder-sprites-in-main_scenegd
Apr 16, 2026
Merged

Fix main scene orphan leaks and tune parallax decor with tests#550
ikostan merged 32 commits intomainfrom
orphan-node-leak-from-placeholder-sprites-in-main_scenegd

Conversation

@ikostan
Copy link
Copy Markdown
Owner

@ikostan ikostan commented Apr 16, 2026

PR Summary: Parallax Performance Optimization and Test Suite Refactor

This Pull Request addresses critical FPS drops related to background generation and standardizes the project's unit testing architecture to ensure long-term stability and memory integrity.

1. Performance Optimizations

  • Parallax "Goldilocks Zone" Implementation: Optimized the background layer heights from 20 screens down to 8 screens. This significantly reduces the Bounding Volume Hierarchy (BVH) overhead and restores a high FPS buffer during gameplay.
  • Sprite Density Reduction: Adjusted the background sprite multiplier from 5x to 2x. This maintains visual density while drastically cutting draw calls and CPU processing time during layer initialization.
  • Centralized Tuning: Introduced an @export variable parallax_screens_tall in main_scene.gd. This allows for real-time performance tuning of all background layers via the Godot Inspector without modifying code.

2. Memory & Stability Fixes

  • Orphan Node Elimination: Refactored the cleanup loops in setup_bushes_layer() and setup_decor_layer() to use an immediate free() strategy. By detaching children with remove_child() and instantly incinerating them, we have eliminated the "orphan window" bug where nodes persisted until the end of a frame.
  • Safe Lifecycle Management: Implemented a standardized safe_hard_free helper to prevent "previously freed" object errors and engine crashes during rapid scene transitions and unit test teardowns.

3. Test Suite Enhancements

  • Consolidated Testing Architecture: Merged redundant test files into test_main_scene_parallax_and_performance.gd to reduce maintenance overhead.
  • Centralized Test Utilities: Integrated shared cleanup logic and mock builders into gut_test_helper.gd. All test suites now utilize this shared helper for consistent teardown behavior.
  • Robust Performance Assertions:
    • Updated execution time tests with a looser threshold (2ms-5ms) to prevent false positives in CI environments while still catching script regressions.
    • Improved randomization tests to verify that flip_h and flip_v are actively producing varied results rather than just checking data types.
  • Strict Type Compliance: Added explicit -> bool return types to all anonymous lambda functions within the test suite to satisfy strict GDScript type-checking requirements.

name: Default Pull Request Template
about: Suggesting changes to SkyLockAssault
title: ''
labels: ''
assignees: ''

Description

What does this PR do? (e.g., "Fixes player jump physics in level 2" or "Adds
new enemy AI script")

Related Issue

Closes #ISSUE_NUMBER (if applicable)

Changes

  • List key changes here (e.g., "Updated Jump.gd to use Godot 4.4's new Tween
    system")
  • Any breaking changes? (e.g., "Deprecated old signal; migrate to new one")

Testing

  • Ran the game in Godot v4.5 editor—describe what you tested (e.g., "Jump
    works on Win10 with 60 FPS")
  • Any new unit tests added? (Link to test scene if yes)
  • Screenshots/GIFs if UI-related: (Attach below)

Checklist

  • Code follows Godot style guide (e.g., snake_case for variables)
  • No console errors in editor/output
  • Ready for review!

Additional Notes

Anything else? (e.g., "Tested on Win10 64-bit; needs Linux validation")

image image

Summary by Sourcery

Resolve orphan node leaks and enforce performance and visual constraints for the main scene’s parallax background and player lifecycle.

New Features:

  • Add randomized rotation, scaling, and flipping for decor sprites in the main scene.

Bug Fixes:

  • Fix orphan node leaks by properly clearing parallax layer children and synchronizing queue_free cleanup with frame processing in tests.
  • Ensure player lifecycle tests cleanly free the main scene without breaking the SceneTree or leaving lingering nodes.

Enhancements:

  • Tune parallax bushes and decor layers to use an 8-screen chunk height with a 2x sprite density multiplier for better infinite scrolling and visual variety.
  • Improve decor layer visuals with wider scale variance and strict cardinal rotations while maintaining performance.

Tests:

  • Add comprehensive GUT test suites to validate main scene orphan-node behavior, parallax chunk sizing and density, decor layer transformations, and main scene _process performance.
  • Tighten existing player lifecycle tests to assert signal disconnection and orphan-free teardown with explicit frame flushing.

Summary by Sourcery

Resolve orphan node leaks in the main scene while enforcing performance and visual constraints on its parallax background layers and tightening player lifecycle cleanup.

New Features:

  • Introduce randomized rotation, scaling, and horizontal/vertical flipping for decor sprites in the main scene to increase visual variety.

Bug Fixes:

  • Prevent orphan node leaks by hard-freeing detached parallax children and synchronizing scene teardown with frame processing in tests.
  • Ensure player lifecycle tests disconnect signals without breaking the SceneTree and cleanly free the main scene between runs.

Enhancements:

  • Standardize parallax bushes and decor layers to an 8-screen chunk height with a 2x sprite density multiplier to balance infinite scrolling visuals with performance.
  • Refine decor layer appearance with a wider uniform scale range and constrained cardinal rotations for consistent, varied scenery.

Tests:

  • Add GUT test suites to validate main scene orphan-node behavior, parallax chunk sizing and density, decor layer transformations, and _process execution time.
  • Strengthen existing player lifecycle tests with explicit frame flushing and stricter assertions around signal disconnection and orphan-free teardown.

Summary by CodeRabbit

  • New Features
    • Denser, taller parallax vegetation and decor with increased variety (larger vertical span, doubled instances, wider scale range, random flips and cardinal rotations).
  • Bug Fixes
    • More immediate cleanup of old layer nodes and removal of debug printouts for cleaner runtime and reduced orphan-node buildup.
  • Tests
    • Added comprehensive GUT suites validating layer density, transforms, lifecycle stability, orphan-node absence, and performance limits.

ikostan added 2 commits April 16, 2026 12:25
Implement child.free(), the "phantom orphan" edge case vanishes entirely. It instantly incinerates the placeholders, meaning you won't even need to add await get_tree().process_frame to your GUT tests. The memory will be clean the moment the setup function finishes executing.
This test plan validates the fix for orphan node leaks caused by improper node cleanup in setup_bushes_layer() and setup_decor_layer().
@ikostan ikostan self-assigned this Apr 16, 2026
@ikostan ikostan added bug Something isn't working enhancement New feature or request labels Apr 16, 2026
@ikostan ikostan linked an issue Apr 16, 2026 that may be closed by this pull request
@ikostan ikostan moved this to In Progress in Sky Lock Assault Project Apr 16, 2026
@sourcery-ai
Copy link
Copy Markdown
Contributor

sourcery-ai Bot commented Apr 16, 2026

Reviewer's Guide

Fixes orphan node leaks and enforces performance/visual constraints for the main scene’s parallax background while tightening player lifecycle cleanup and adding targeted GUT coverage.

Class diagram for main_scene parallax setup and decor sprites

classDiagram
    class MainScene {
        - bool _showing_unbound_warning
        - bool _showing_unbound_key_message
        + Node2D player
        + Panel stats_panel
        + ParallaxBackground background
        + ParallaxLayer bushes_layer
        + ParallaxLayer decor_layer
        + void setup_bushes_layer(viewport: Vector2)
        + void setup_decor_layer(viewport: Vector2)
        + void show_message(text: String, type: MessageType)
    }

    class ParallaxBackground {
        + Vector2 scroll_base_offset
        + Vector2 scroll_base_scale
    }

    class ParallaxLayer {
        + Vector2 motion_scale
        + Vector2 motion_mirroring
        + Array get_children()
        + void add_child(node: Node)
        + void remove_child(node: Node)
    }

    class Sprite2D {
        + Texture2D texture
        + bool centered
        + Vector2 scale
        + bool flip_h
        + bool flip_v
        + float rotation
        + Vector2 position
    }

    class TexturePreloader {
        + Array get_resource_list()
        + Texture2D get_resource(id: String)
    }

    MainScene --> ParallaxBackground : has
    MainScene --> ParallaxLayer : uses_bushes_layer
    MainScene --> ParallaxLayer : uses_decor_layer
    MainScene --> TexturePreloader : uses
    MainScene --> Sprite2D : creates
    ParallaxBackground --> ParallaxLayer : contains
Loading

Flow diagram for setup_decor_layer decor generation and cleanup

flowchart TD
    A["setup_decor_layer(viewport)"] --> B{decor_layer exists?}
    B -- "no" --> Z["return"]
    B -- "yes" --> C["for each child in decor_layer.get_children()"]
    C --> D["decor_layer.remove_child(child)"]
    D --> E["child.free()"]
    E --> F["collect decor_ids from texture_preloader where id begins_with decor_"]
    F --> G{decor_ids is_empty?}
    G -- "yes" --> Z
    G -- "no" --> H["screens_tall = 8.0"]
    H --> I["layer_height = viewport.y * screens_tall"]
    I --> J["num_decors = decor_ids.size() * 2"]
    J --> K["allowed_rotations = [0.0, 90.0, 180.0, -90.0]"]
    K --> L["for i in range(num_decors)"]
    L --> M["decor = Sprite2D.new()"]
    M --> N["id = decor_ids.pick_random()"]
    N --> O["decor.texture = texture_preloader.get_resource(id)"]
    O --> P["decor.centered = false"]
    P --> Q["scale_factor = randf_range(0.5, 1.5)"]
    Q --> R["decor.scale = Vector2(scale_factor, scale_factor)"]
    R --> S["decor.flip_h = randf() < 0.5"]
    S --> T["decor.flip_v = randf() < 0.5"]
    T --> U["random_degrees = allowed_rotations.pick_random()"]
    U --> V["decor.rotation = deg_to_rad(random_degrees)"]
    V --> W["decor.position.x = randf_range(0, viewport.x - decor.texture.get_width() * scale_factor)"]
    W --> X["decor.position.y = randf_range(0, layer_height - decor.texture.get_height() * scale_factor)"]
    X --> Y["decor_layer.add_child(decor)"]
    Y --> L
    L --> AA["decor_layer.motion_mirroring = Vector2(0, layer_height)"]
    AA --> Z
Loading

File-Level Changes

Change Details Files
Adjust parallax bushes and decor setup to destroy old children immediately and tune chunk height/density and visual variety.
  • Switch from queue_free() to free() after removing children from parallax bushes and decor layers to avoid delayed-orphan buildup.
  • Increase parallax layer height to 8 viewport screens and set motion_mirroring.y accordingly for both bushes and decor.
  • Change bush and decor instance counts to use a 2x multiplier of available preloaded resources for controlled density.
  • Extend decor setup to randomize uniform scale over a wider 0.5–1.5 range, add horizontal/vertical flip randomization, and restrict rotations to cardinal angles using a pick_random() over a fixed list.
  • Remove debug print statements that logged loaded bush/decor texture counts.
scripts/main_scene.gd
Tighten player lifecycle tests to free scenes deterministically and ensure signal disconnection and frame flushing behave correctly.
  • Update test teardown to prefer hard free() over queue_free() for main_scene to avoid lingering nodes in GUT orphan checks.
  • In the signal-severing test, call player_root._exit_tree() directly instead of removing the node from the tree, then assert settings signals are disconnected.
  • Insert an awaited process_frame after invoking _exit_tree() to allow queued frees from MainScene._ready() to complete before GUT inspects orphans.
  • Clarify test names and comments to emphasize safety, SceneTree integrity, and null Globals.settings handling.
test/gut/test_player_lifecycle.gd
Add GUT tests to enforce main scene parallax configuration, decor transformations, orphan-node behavior, and _process performance bounds.
  • Introduce test_main_scene_parallax_and_performance.gd to instantiate MainScene in a controlled viewport, verify 8-screen motion_mirroring height, enforce a 2x density multiplier for bushes/decor, and assert average _process execution time stays under a loose microsecond threshold.
  • Add test_decor_layer_transformations.gd to validate decor sprites use only cardinal rotations, maintain uniform scale within 0.5–1.5, and exhibit both true/false states for flip_h and flip_v across the generated set.
  • Add test_main_scene_orphan_nodes.gd to exercise repeated layer setups, scene reloads, and stress loops while asserting orphan-node counts return to a captured baseline via Performance monitors.
  • Use a shared safe_hard_free() helper and before_each/after_each patterns in new test files to remove instantiated scenes safely and avoid double-free issues, with corresponding .uid files for Godot/GUT integration.
test/gut/test_main_scene_parallax_and_performance.gd
test/gut/test_decor_layer_transformations.gd
test/gut/test_main_scene_orphan_nodes.gd
test/gut/test_decor_layer_transformations.gd.uid
test/gut/test_main_scene_orphan_nodes.gd.uid
test/gut/test_main_scene_parallax_and_performance.gd.uid

Assessment against linked issues

Issue Objective Addressed Explanation
#540 Update setup_bushes_layer() and setup_decor_layer() in main_scene.gd to clear existing children using an immediate free() (instead of remove_child() + queue_free()) so placeholder Sprite2D nodes do not become orphans.
#540 Modify test_player_lifecycle.gd so that test teardown uses a hard main_scene.free() instead of queue_free(), and adjust lifecycle tests to manually call player_root._exit_tree() (rather than remove_child()) while ensuring cleanup does not leave orphan nodes.
#549 Ensure setup_bushes_layer() and setup_decor_layer() clean up their previous children without leaving persistent orphan nodes.
#549 Add automated GUT tests that implement the orphan-node QA test plan scenarios (teardown memory sync, repeated setup, immediate rebuild integrity, scene reload lifecycle, stress input, and layer isolation) with appropriate frame syncing.
#549 Align test and teardown behavior with Godot’s deletion semantics (queue_free vs free), ensuring orphan checks occur after appropriate frame processing and scenes are fully freed.
#551 Modify setup_decor_layer() in main_scene.gd so that spawned decor sprites receive a randomized rotation to break up visual repetition in the main scene background.
#552 Increase the vertical size (layer_height) of the parallax bushes and decor background chunks in main_scene.gd so they are significantly taller than the current viewport.y * 4 to reduce visible repetition.
#552 Increase the number of parallax bush and decor sprites proportionally to the new chunk height so that visual density is maintained across the expanded area.

Possibly linked issues

  • #N/A: PR replaces remove_child+queue_free with free in main_scene and uses hard free in tests, matching the issue’s fixes.
  • #N/A: PR directly implements the orphan-node cleanup behavior and adds GUT tests corresponding to the described QA test plan.

Tips and commands

Interacting with Sourcery

  • Trigger a new review: Comment @sourcery-ai review on the pull request.
  • Continue discussions: Reply directly to Sourcery's review comments.
  • Generate a GitHub issue from a review comment: Ask Sourcery to create an
    issue from a review comment by replying to it. You can also reply to a
    review comment with @sourcery-ai issue to create an issue from it.
  • Generate a pull request title: Write @sourcery-ai anywhere in the pull
    request title to generate a title at any time. You can also comment
    @sourcery-ai title on the pull request to (re-)generate the title at any time.
  • Generate a pull request summary: Write @sourcery-ai summary anywhere in
    the pull request body to generate a PR summary at any time exactly where you
    want it. You can also comment @sourcery-ai summary on the pull request to
    (re-)generate the summary at any time.
  • Generate reviewer's guide: Comment @sourcery-ai guide on the pull
    request to (re-)generate the reviewer's guide at any time.
  • Resolve all Sourcery comments: Comment @sourcery-ai resolve on the
    pull request to resolve all Sourcery comments. Useful if you've already
    addressed all the comments and don't want to see them anymore.
  • Dismiss all Sourcery reviews: Comment @sourcery-ai dismiss on the pull
    request to dismiss all existing Sourcery reviews. Especially useful if you
    want to start fresh with a new review - don't forget to comment
    @sourcery-ai review to trigger a new review!

Customizing Your Experience

Access your dashboard to:

  • Enable or disable review features such as the Sourcery-generated pull request
    summary, the reviewer's guide, and others.
  • Change the review language.
  • Add, remove or edit custom review instructions.
  • Adjust other review settings.

Getting Help

@ikostan ikostan linked an issue Apr 16, 2026 that may be closed by this pull request
@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented Apr 16, 2026

📝 Walkthrough

Walkthrough

Replaced deferred frees with immediate free() in parallax layer setup, doubled vertical tiling and sprite counts for bushes/decor, added random flips/scales/90° rotations to decor sprites, tightened stats_panel type, and introduced multiple GUT test suites to validate orphan-node behavior, parallax chunking, transformations, and performance.

Changes

Cohort / File(s) Summary
Main scene runtime
scripts/main_scene.gd
Switched queue_free()free() when clearing layer children; removed debug prints; layer height set to viewport.y * 8.0; instance counts doubled; decor sprites gain random flip, uniform scale range 0.5–1.5, and random cardinal rotation; stats_panel typed as Panel.
Orphan-node tests
test/gut/test_main_scene_orphan_nodes.gd, test/gut/test_main_scene_orphan_nodes.gd.uid
New comprehensive GUT suite exercising repeated setup/teardown, immediate rebuilds, layer isolation, scene reloads, and stress loops to assert no increase in Performance.OBJECT_ORPHAN_NODE_COUNT.
Decor transformation tests
test/gut/test_decor_layer_transformations.gd, test/gut/test_decor_layer_transformations.gd.uid
New tests verifying decor sprites’ cardinal rotations, uniform scale within 0.5–1.5, and boolean flip flags.
Parallax chunk & density tests
test/gut/test_main_scene_parallax_chunks.gd, test/gut/test_main_scene_parallax_chunks.gd.uid
New tests asserting motion_mirroring.y == viewport.y * 8.0 and that active sprite counts equal 2× resource counts for bush/decor types.
Performance limits tests
test/gut/test_main_scene_performance_limits.gd, test/gut/test_main_scene_performance_limits.gd.uid
New tests checking mirrored chunk sizing, doubled sprite density, and _process execution time budget across simulated frames.
Player lifecycle test tweak
test/gut/test_player_lifecycle.gd
Per-test teardown now restores Globals.settings and calls main_scene.free(); added frame sync after _exit_tree() to allow deferred cleanup; adjusted assertions/comments.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Possibly related issues

Poem

🐰 I hopped through code and cleared the brush,
I flipped and twirled each sprite in a rush,
I freed the strays so none remain,
I wrote the tests to catch the bane,
Carrots for all — no orphaned pain. 🥕✨

🚥 Pre-merge checks | ✅ 3
✅ Passed checks (3 passed)
Check name Status Explanation
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Title check ✅ Passed The title accurately reflects the main changes: fixing orphan node leaks in the main scene and tuning parallax decor layers with comprehensive tests.
Description check ✅ Passed The PR description includes a comprehensive overview of changes, related objective summary, performance improvements, memory fixes, and testing enhancements with context images.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch orphan-node-leak-from-placeholder-sprites-in-main_scenegd

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@ikostan ikostan changed the title Orphan node leak from placeholder sprites in main scenegd Orphan node leak from placeholder sprites in main scene Apr 16, 2026
Copy link
Copy Markdown
Contributor

@sourcery-ai sourcery-ai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hey - I've left some high level feedback:

  • The switch from remove_child(child); child.queue_free() to if is_instance_valid(child): child.free() is a bit risky; in Godot scenes it’s usually safer and more idiomatic to just call child.queue_free() on each child without manually removing it, rather than using free() directly while iterating the node tree.
  • There’s now a fair amount of duplicated Performance.get_monitor(Performance.OBJECT_ORPHAN_NODE_COUNT) baseline/compare logic across the new tests; consider extracting a small helper or utility assertion to keep each test focused on the scenario rather than the boilerplate leak-check code.
  • The commented-out remove_child and print lines in setup_bushes_layer/setup_decor_layer add noise; if they’re no longer needed, it would be cleaner to remove them or gate the logging behind a debug flag instead of leaving them commented out.
Prompt for AI Agents
Please address the comments from this code review:

## Overall Comments
- The switch from `remove_child(child); child.queue_free()` to `if is_instance_valid(child): child.free()` is a bit risky; in Godot scenes it’s usually safer and more idiomatic to just call `child.queue_free()` on each child without manually removing it, rather than using `free()` directly while iterating the node tree.
- There’s now a fair amount of duplicated `Performance.get_monitor(Performance.OBJECT_ORPHAN_NODE_COUNT)` baseline/compare logic across the new tests; consider extracting a small helper or utility assertion to keep each test focused on the scenario rather than the boilerplate leak-check code.
- The commented-out `remove_child` and `print` lines in `setup_bushes_layer`/`setup_decor_layer` add noise; if they’re no longer needed, it would be cleaner to remove them or gate the logging behind a debug flag instead of leaving them commented out.

Sourcery is free for open source - if you like our reviews please consider sharing them ✨
Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.

@deepsource-io
Copy link
Copy Markdown

deepsource-io Bot commented Apr 16, 2026

DeepSource Code Review

We reviewed changes in 6c4acc0...b9cfc17 on this pull request. Below is the summary for the review, and you can see the individual issues we found as inline review comments.

See full review on DeepSource ↗

PR Report Card

Overall Grade   Security  

Reliability  

Complexity  

Hygiene  

Code Review Summary

Analyzer Status Updated (UTC) Details
Python Apr 16, 2026 10:43p.m. Review ↗
JavaScript Apr 16, 2026 10:43p.m. Review ↗

Important

AI Review is run only on demand for your team. We're only showing results of static analysis review right now. To trigger AI Review, comment @deepsourcebot review on this thread.

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🧹 Nitpick comments (4)
scripts/main_scene.gd (2)

164-173: Same dead-code cleanup applies here.

Lines 165 and 173 mirror the commented-out fragments in setup_bushes_layer. Apply the same removal for consistency.

♻️ Proposed cleanup
 	# Clear existing children
 	for child in decor_layer.get_children():
-		# decor_layer.remove_child(child)
 		if is_instance_valid(child):
 			child.free()

 	# Get decor IDs from preloader (Array[String])
 	var decor_ids: Array = Array(texture_preloader.get_resource_list()).filter(
 		func(id: String) -> bool: return id.begins_with("decor_")
 	)
-	# print("Loaded ", decor_ids.size(), " decor textures")
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@scripts/main_scene.gd` around lines 164 - 173, Remove the dead commented
lines in the decor cleanup block to match the cleanup performed in
setup_bushes_layer: delete the commented-out decor_layer.remove_child(child)
inside the for loop and the commented-out print("Loaded ", decor_ids.size(), "
decor textures") after building decor_ids; keep the is_instance_valid(child)
check and child.free() logic and the decor_ids construction using
texture_preloader so behavior remains unchanged.

120-129: Remove dead/commented-out code rather than leaving it behind.

The commented-out bushes_layer.remove_child(child) (Line 121) and print("Loaded ", bush_ids.size(), ...) (Line 129) are now dead code. Version control preserves history — leaving them as comments adds noise and invites confusion about whether they’re intentional placeholders. Either delete them or replace with a short rationale comment.

♻️ Proposed cleanup
 	# Clear existing children
 	for child in bushes_layer.get_children():
-		# bushes_layer.remove_child(child)
 		if is_instance_valid(child):
 			child.free()

 	# Get bush IDs from preloader (Array[String])
 	var bush_ids: Array = Array(texture_preloader.get_resource_list()).filter(
 		func(id: String) -> bool: return id.begins_with("bush_")
 	)
-	# print("Loaded ", bush_ids.size(), " bush textures")
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@scripts/main_scene.gd` around lines 120 - 129, The commented-out lines in the
block that iterates over bushes_layer and gathers bush_ids are dead code; remove
the commented-out bushes_layer.remove_child(child) and the print statement (or
replace each with a one-line rationale comment if you intend to keep the intent)
to reduce noise. Update the section around the for loop that frees children
(referencing bushes_layer and is_instance_valid) and the bush texture collection
that uses texture_preloader and bush_ids so the code contains only active logic
or a brief justification comment.
test/gut/test_main_scene_orphan_nodes.gd (2)

42-43: Explicit queue_free() on an autofree-tracked node — minor redundancy.

before_each already registers main_scene with add_child_autofree, so the node will be freed at test teardown regardless. Manually calling queue_free() here is fine (GUT checks validity before auto-freeing), but it couples the test to internal autofree behavior. For lifecycle tests that specifically want to observe post-free state, consider using add_child(main_scene) (non-autofree) in these tests and managing the lifecycle explicitly, so the intent is clear and there’s no possibility of double-free across GUT versions.

Same pattern on Line 124.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@test/gut/test_main_scene_orphan_nodes.gd` around lines 42 - 43, The test
explicitly calls main_scene.queue_free() then awaits get_tree().process_frame
while before_each already registers main_scene with add_child_autofree, causing
redundant coupling to autofree behavior; change tests that intend to observe
post-free state to use add_child(main_scene) instead of add_child_autofree in
the before_each (or in the specific test setup) so you manage the node lifecycle
explicitly, remove the redundant reliance on autofree, and keep the queue_free()
+ await get_tree().process_frame sequence only in tests that own the node’s
lifetime; update the setup referencing before_each, add_child_autofree,
add_child, main_scene, queue_free, and get_tree().process_frame accordingly.

29-46: Use assert_lte instead of assert_eq for engine-wide orphan counter.

Performance.OBJECT_ORPHAN_NODE_COUNT is engine-wide and reports orphans across the entire engine, not just nodes in this test. During the awaited frames, unrelated systems may transiently create or free orphan nodes, causing flaky assertions with strict equality. Leak regression checks are better expressed as "did not grow" rather than "exactly equal".

Apply to lines 46, 65, 136, and 156:

Diff
-	assert_eq(current_orphans, baseline_orphans, "Expected orphan nodes to return to baseline after frame sync and teardown.")
+	assert_lte(current_orphans, baseline_orphans, "Orphan node count must not grow after frame sync and teardown.")
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@test/gut/test_main_scene_orphan_nodes.gd` around lines 29 - 46, Replace
strict equality checks against the engine-wide orphan counter with a
non-increasing assertion: where the test captures baseline_orphans via
Performance.get_monitor(Performance.OBJECT_ORPHAN_NODE_COUNT) and later computes
current_orphans, change the assertion from assert_eq(current_orphans,
baseline_orphans, ...) to assert_lte(current_orphans, baseline_orphans, ...)
(apply the same replacement for the other occurrences that compare
Performance.OBJECT_ORPHAN_NODE_COUNT at the indicated spots). This ensures the
test verifies the orphan count did not grow rather than requiring exact equality
across engine-wide, transient activity.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@test/gut/test_main_scene_orphan_nodes.gd`:
- Around line 38-39: The comment next to "await get_tree().process_frame" is
outdated because production now uses immediate removal via child.free() not
queue_free(); update the test comment to reflect that the frame flush is no
longer required and explain that get_tree().process_frame was previously used to
wait for queue_free() but is unnecessary after main_scene.gd was changed to call
child.free() — locate the await in test/gut/test_main_scene_orphan_nodes.gd and
replace the rationale text to state that immediate freeing is used and the await
can be removed or kept only for clarity, referencing get_tree().process_frame,
queue_free(), child.free(), and main_scene.gd so reviewers understand the
change.

---

Nitpick comments:
In `@scripts/main_scene.gd`:
- Around line 164-173: Remove the dead commented lines in the decor cleanup
block to match the cleanup performed in setup_bushes_layer: delete the
commented-out decor_layer.remove_child(child) inside the for loop and the
commented-out print("Loaded ", decor_ids.size(), " decor textures") after
building decor_ids; keep the is_instance_valid(child) check and child.free()
logic and the decor_ids construction using texture_preloader so behavior remains
unchanged.
- Around line 120-129: The commented-out lines in the block that iterates over
bushes_layer and gathers bush_ids are dead code; remove the commented-out
bushes_layer.remove_child(child) and the print statement (or replace each with a
one-line rationale comment if you intend to keep the intent) to reduce noise.
Update the section around the for loop that frees children (referencing
bushes_layer and is_instance_valid) and the bush texture collection that uses
texture_preloader and bush_ids so the code contains only active logic or a brief
justification comment.

In `@test/gut/test_main_scene_orphan_nodes.gd`:
- Around line 42-43: The test explicitly calls main_scene.queue_free() then
awaits get_tree().process_frame while before_each already registers main_scene
with add_child_autofree, causing redundant coupling to autofree behavior; change
tests that intend to observe post-free state to use add_child(main_scene)
instead of add_child_autofree in the before_each (or in the specific test setup)
so you manage the node lifecycle explicitly, remove the redundant reliance on
autofree, and keep the queue_free() + await get_tree().process_frame sequence
only in tests that own the node’s lifetime; update the setup referencing
before_each, add_child_autofree, add_child, main_scene, queue_free, and
get_tree().process_frame accordingly.
- Around line 29-46: Replace strict equality checks against the engine-wide
orphan counter with a non-increasing assertion: where the test captures
baseline_orphans via
Performance.get_monitor(Performance.OBJECT_ORPHAN_NODE_COUNT) and later computes
current_orphans, change the assertion from assert_eq(current_orphans,
baseline_orphans, ...) to assert_lte(current_orphans, baseline_orphans, ...)
(apply the same replacement for the other occurrences that compare
Performance.OBJECT_ORPHAN_NODE_COUNT at the indicated spots). This ensures the
test verifies the orphan count did not grow rather than requiring exact equality
across engine-wide, transient activity.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

Run ID: 741764e1-e1d9-4e45-9f22-dbe5e8b69598

📥 Commits

Reviewing files that changed from the base of the PR and between 6c4acc0 and faa58f0.

📒 Files selected for processing (3)
  • scripts/main_scene.gd
  • test/gut/test_main_scene_orphan_nodes.gd
  • test/gut/test_main_scene_orphan_nodes.gd.uid
📜 Review details
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (3)
  • GitHub Check: GDUnit4 Unit Tests / unit-test
  • GitHub Check: GUT Unit Tests / unit-test
  • GitHub Check: Sourcery review
🧰 Additional context used
🧠 Learnings (1)
📚 Learning: 2026-03-30T04:02:23.747Z
Learnt from: ikostan
Repo: ikostan/SkyLockAssault PR: 500
File: test/gut/test_audio_web_bridge.gd:131-145
Timestamp: 2026-03-30T04:02:23.747Z
Learning: In GUT (Godot Unit Test) for Godot 4, when using `assert_called` / `assert_called_count` with parameter matching, include *every* argument the mocked method accepts, including parameters with default values. GUT does not auto-fill default arguments during call matching. For example, if `JavaScriptBridgeWrapper.eval(script: String, global_exec: bool = false)` is invoked as `eval(js_string)`, the actual call recorded by GUT includes the default (`eval(js_string, false)`), so your assertion must match both arguments (e.g., `.bind(js_string, false)`, not `.bind(js_string)`). Apply this rule to GUT assertions in `test/gut` tests.

Applied to files:

  • test/gut/test_main_scene_orphan_nodes.gd
🔇 Additional comments (2)
test/gut/test_main_scene_orphan_nodes.gd (1)

16-21: LGTM — async before_each with frame flush is the right pattern.

Using add_child_autofree plus a one-frame await lets _ready() run (including the initial setup_bushes_layer/setup_decor_layer calls) before each test exercises the cleanup path. Good setup.

test/gut/test_main_scene_orphan_nodes.gd.uid (1)

1-1: Auto-generated UID — no review needed.

Standard Godot 4 resource UID mapping for the new test script.

Comment thread test/gut/test_main_scene_orphan_nodes.gd Outdated
ikostan and others added 2 commits April 16, 2026 12:39
Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
This is the idiomatic and safe pattern in Godot because:

queue_free() defers deletion → avoids modifying the scene tree mid-iteration
Prevents:
iterator invalidation
hard-to-reproduce crashes
undefined behavior in complex trees
Lets Godot handle safe teardown order internally
@ikostan
Copy link
Copy Markdown
Owner Author

ikostan commented Apr 16, 2026

  • The switch from remove_child(child); child.queue_free() to if is_instance_valid(child): child.free() is a bit risky; in Godot scenes it’s usually safer and more idiomatic to just call child.queue_free() on each child without manually removing it, rather than using free() directly while iterating the node tree.

Addressed

Introduce verify_no_orphan_leaks() to centralize orphan-node assertions and replace scattered Performance.get_monitor checks across tests. Harden tests by flushing frames at key points (to allow queue_free()/_ready() work to settle), filter out nodes queued for deletion when comparing child counts, and add minor assertions/log output to improve clarity. These changes reduce flakiness and better detect true orphan-node leaks in test/gut/test_main_scene_orphan_nodes.gd.
@ikostan
Copy link
Copy Markdown
Owner Author

ikostan commented Apr 16, 2026

  • There’s now a fair amount of duplicated Performance.get_monitor(Performance.OBJECT_ORPHAN_NODE_COUNT) baseline/compare logic across the new tests; consider extracting a small helper or utility assertion to keep each test focused on the scenario rather than the boilerplate leak-check code.

Addressed

The commented-out remove_child and print lines in setup_bushes_layer/setup_decor_layer add noise; if they’re no longer needed, it would be cleaner to remove them or gate the logging behind a debug flag instead of leaving them commented out.
@ikostan
Copy link
Copy Markdown
Owner Author

ikostan commented Apr 16, 2026

  • The commented-out remove_child and print lines in setup_bushes_layer/setup_decor_layer add noise; if they’re no longer needed, it would be cleaner to remove them or gate the logging behind a debug flag instead of leaving them commented out.

Addressed

@ikostan
Copy link
Copy Markdown
Owner Author

ikostan commented Apr 16, 2026

@sourcery-ai review

Copy link
Copy Markdown
Contributor

@sourcery-ai sourcery-ai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hey - I've found 1 issue, and left some high level feedback:

  • Consider giving stats_panel a concrete type (e.g., Panel or a specific custom type) instead of Variant now that the leak issue is fixed, so you retain static checking and editor hints on that node.
  • The orphan-node tests depend on the global Performance.OBJECT_ORPHAN_NODE_COUNT and the shared SceneTree, which can be polluted by other tests or scenes; you may want to isolate these tests (e.g., with a dedicated SceneTree or by ensuring no other test scenes are active) to avoid flaky baselines.
Prompt for AI Agents
Please address the comments from this code review:

## Overall Comments
- Consider giving `stats_panel` a concrete type (e.g., `Panel` or a specific custom type) instead of `Variant` now that the leak issue is fixed, so you retain static checking and editor hints on that node.
- The orphan-node tests depend on the global `Performance.OBJECT_ORPHAN_NODE_COUNT` and the shared SceneTree, which can be polluted by other tests or scenes; you may want to isolate these tests (e.g., with a dedicated SceneTree or by ensuring no other test scenes are active) to avoid flaky baselines.

## Individual Comments

### Comment 1
<location path="scripts/main_scene.gd" line_range="15-18" />
<code_context>

 @onready var player: Node2D = $Player
-# @onready var stats_panel: Panel = $PlayerStatsPanel
 @onready var stats_panel: Variant = $PlayerStatsPanel
 @onready var background: ParallaxBackground = $Background
 @onready var bushes_layer: ParallaxLayer = $Background/Bushes  # Reference to the bushes layer
</code_context>
<issue_to_address>
**suggestion:** Consider tightening the type of `stats_panel` instead of using `Variant`.

If this is always a specific node type, prefer a concrete type to retain static checks and editor autocompletion. If you need to support multiple node types, consider typing it to a shared base class (e.g., `Control`) or an interface-like abstraction instead of falling back to `Variant`.

```suggestion
@onready var player: Node2D = $Player
@onready var stats_panel: Panel = $PlayerStatsPanel
@onready var background: ParallaxBackground = $Background
@onready var bushes_layer: ParallaxLayer = $Background/Bushes  # Reference to the bushes layer
```
</issue_to_address>

Sourcery is free for open source - if you like our reviews please consider sharing them ✨
Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.

Comment thread scripts/main_scene.gd
…ing Variant.

If this is always a specific node type, prefer a concrete type to retain static checks and editor autocompletion. If you need to support multiple node types, consider typing it to a shared base class (e.g., Control) or an interface-like abstraction instead of falling back to Variant.
@ikostan
Copy link
Copy Markdown
Owner Author

ikostan commented Apr 16, 2026

  • Consider giving stats_panel a concrete type (e.g., Panel or a specific custom type) instead of Variant now that the leak issue is fixed, so you retain static checking and editor hints on that node.

Addressed

ikostan added 3 commits April 16, 2026 14:30
Reduce repeating layer height from 20 screens to 8 for bushes and decor to balance the infinite-scrolling illusion with CPU overhead. Lower the density multipliers from 5x to 2x (num_bushes and num_decors) so perceived density matches the smaller repeated area. Comments updated to explain the rationale; no changes to rotation logic.
The Structural Asserts: test_parallax_chunk_size_is_optimized and test_parallax_sprite_density_is_optimized explicitly enforce the 8-screen limit and the 2x multiplier we just set. They act as automated guardrails against future developers accidentally re-introducing the BVH bloat that caused your FPS drop.

The Execution Proxy: test_process_script_execution_time is a neat trick. It fires the _process() function 60 times in a row as fast as possible and measures the raw CPU time in microseconds. While it doesn't measure the GPU rendering the sprites, it does measure the GDScript overhead, ensuring your math and checks inside _process stay incredibly fast.
@ikostan
Copy link
Copy Markdown
Owner Author

ikostan commented Apr 16, 2026

@sourcery-ai review

Copy link
Copy Markdown
Contributor

@sourcery-ai sourcery-ai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hey - I've found 1 issue, and left some high level feedback:

  • The test_decor_sprites_have_boolean_flips test only checks that flip_h/flip_v are of type bool (which they always are by default) rather than that random flips are actually being applied; consider asserting that at least some sprites have true and some have false to make this test meaningful.
  • Parallax chunk size and density behaviors (8-screen height and 2x density) are asserted in multiple suites (test_main_scene_parallax_chunks.gd, test_main_scene_performance_limits.gd, etc.); consolidating these checks into a single focused test file would reduce duplication and future maintenance overhead if those parameters change.
  • Test teardown strategies vary between files (some rely on autofree and frame flushes while others call free() explicitly); standardizing on a single, clearly documented cleanup pattern for MainScene tests would make lifecycle behavior easier to reason about and reduce the risk of subtle orphan-node issues reappearing.
Prompt for AI Agents
Please address the comments from this code review:

## Overall Comments
- The `test_decor_sprites_have_boolean_flips` test only checks that `flip_h`/`flip_v` are of type `bool` (which they always are by default) rather than that random flips are actually being applied; consider asserting that at least some sprites have `true` and some have `false` to make this test meaningful.
- Parallax chunk size and density behaviors (8-screen height and 2x density) are asserted in multiple suites (`test_main_scene_parallax_chunks.gd`, `test_main_scene_performance_limits.gd`, etc.); consolidating these checks into a single focused test file would reduce duplication and future maintenance overhead if those parameters change.
- Test teardown strategies vary between files (some rely on `autofree` and frame flushes while others call `free()` explicitly); standardizing on a single, clearly documented cleanup pattern for MainScene tests would make lifecycle behavior easier to reason about and reduce the risk of subtle orphan-node issues reappearing.

## Individual Comments

### Comment 1
<location path="scripts/main_scene.gd" line_range="200-202" />
<code_context>
+		var scale_factor: float = randf_range(0.5, 1.5)
 		decor.scale = Vector2(scale_factor, scale_factor)

+		# SCALING TRICK 2: Randomly mirror the sprite horizontally and/or vertically
+		decor.flip_h = [true, false].pick_random()
+		decor.flip_v = [true, false].pick_random()
+
+		# Apply random cardinal rotation to ALL decor sprites
</code_context>
<issue_to_address>
**suggestion (performance):** Avoid reallocating the `[true, false]` array on every sprite when randomizing flips.

`[true, false].pick_random()` inside the loop allocates a new array for each call, which adds overhead for large `num_decors`. Prefer either a shared constant (e.g. `const BOOL_CHOICES := [true, false]`) or use `randf() < 0.5` for each flip flag to avoid per-iteration allocations while keeping the same behavior.

```suggestion
		# SCALING TRICK 2: Randomly mirror the sprite horizontally and/or vertically
		decor.flip_h = randf() < 0.5
		decor.flip_v = randf() < 0.5
```
</issue_to_address>

Sourcery is free for open source - if you like our reviews please consider sharing them ✨
Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.

Comment thread scripts/main_scene.gd Outdated
ikostan and others added 4 commits April 16, 2026 14:40
Co-authored-by: sourcery-ai[bot] <58596630+sourcery-ai[bot]@users.noreply.github.com>
The PR does not change the child cleanup semantics to eliminate the orphan window. setup_bushes_layer() still has no explicit clear loop shown in the diff, and setup_decor_layer() continues to clear children using decor_layer.remove_child(child) followed by child.queue_free(), rather than calling free() directly. This preserves the timing window where removed children exist as orphans until the end of the frame, which is the core bug described in the issue.
The test_decor_sprites_have_boolean_flips test only checks that flip_h/flip_v are of type bool (which they always are by default) rather than that random flips are actually being applied; consider asserting that at least some sprites have true and some have false to make this test meaningful.
@ikostan
Copy link
Copy Markdown
Owner Author

ikostan commented Apr 16, 2026

  • The test_decor_sprites_have_boolean_flips test only checks that flip_h/flip_v are of type bool (which they always are by default) rather than that random flips are actually being applied; consider asserting that at least some sprites have true and some have false to make this test meaningful.

Addressed

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
scripts/main_scene.gd (1)

194-211: ⚠️ Potential issue | 🟡 Minor

Rotation with centered = false can push decor sprites outside the intended bounds.

decor.centered = false keeps the pivot at the sprite's top-left. Random rotations of 90°, 180°, and -90° are then applied around that top-left pivot, but the position clamp on lines 208–211 only accounts for the unrotated extent (texture.get_width() * scale_factor on X, height on Y). Concretely:

  • 90°: after rotation the sprite occupies [x, x + h*scale] × [y - w*scale, y], so it can extend above the layer (negative Y).
  • 180°: it occupies [x - w*scale, x] × [y - h*scale, y], so it can extend left/above.
  • -90°: it occupies [x - h*scale, x] × [y, y + w*scale], so it can extend left.

For non-square textures this also means horizontal-fit assumes width*scale but a ±90° rotation makes the effective horizontal extent height*scale. Result: decor can render outside the randomized safe area (and, with 180°, even into negative coordinates near the top of the layer).

Simplest fix is to centered = true on decor sprites so rotation pivots around the sprite center, then shrink the clamp by half-extents; alternatively compute the rotated AABB and clamp accordingly.

♻️ Proposed fix (centered pivot)
-		decor.texture = texture_preloader.get_resource(id)
-		decor.centered = false
+		decor.texture = texture_preloader.get_resource(id)
+		decor.centered = true
@@
-		decor.position.x = randf_range(0, viewport.x - (decor.texture.get_width() * scale_factor))
-		decor.position.y = randf_range(
-			0, layer_height - (decor.texture.get_height() * scale_factor)
-		)
+		# With centered pivot, use the max of width/height for the half-extent so any cardinal rotation stays in bounds.
+		var tex_size: Vector2 = decor.texture.get_size()
+		var half_extent: float = max(tex_size.x, tex_size.y) * 0.5 * scale_factor
+		decor.position.x = randf_range(half_extent, viewport.x - half_extent)
+		decor.position.y = randf_range(half_extent, layer_height - half_extent)
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@scripts/main_scene.gd` around lines 194 - 211, The decor sprites are rotated
around the top-left because decor.centered = false, so applying
allowed_rotations.pick_random() and setting decor.rotation can push sprites
outside the intended bounds when you clamp using
texture.get_width()/get_height() * scale_factor; fix this by setting the sprite
pivot to its center (set decor.centered = true) and adjust the position clamps
to account for half-extents (use (texture.get_width() * scale_factor) / 2 and
(texture.get_height() * scale_factor) / 2) before assigning decor.position.x and
decor.position.y so rotations around center stay within the layer.
🧹 Nitpick comments (2)
test/gut/test_main_scene_parallax_chunks.gd (1)

70-102: Consider consolidating the two near-duplicate tests.

test_bushes_layer_chunk_size_and_density and test_decor_layer_chunk_size_and_density differ only by layer reference, ID prefix, and setup method. A small parameterized helper (or gut's parameterize_test) would remove the duplication and keep future mirroring-height / density changes to a single edit. Not blocking.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@test/gut/test_main_scene_parallax_chunks.gd` around lines 70 - 102, The two
near-duplicate tests test_bushes_layer_chunk_size_and_density and
test_decor_layer_chunk_size_and_density should be consolidated by extracting the
common assertions into a single parameterized helper (or using gut's
parameterize_test) that accepts the layer setup method
(main_scene.setup_decor_layer / main_scene.setup_bushes_layer), the layer
accessor (main_scene.decor_layer / main_scene.bushes_layer) and the ID prefix
("decor_" / "bushes_"); re-run the appropriate setup via the passed setup
function, compute expected_height using viewport_mock.y * 8.0, get resource IDs
via texture_preloader.get_resource_list() filtered by the given prefix, compute
expected_count = ids.size() * 2, and assert the active child count (filtering
out is_queued_for_deletion) and motion_mirroring.y against those expected values
so both layer tests are covered by one parameterized test.
test/gut/test_decor_layer_transformations.gd (1)

16-28: Redundant lifecycle: add_child_autofree plus explicit main_scene.free() in after_each.

add_child_autofree() registers main_scene for automatic freeing at test teardown. Calling main_scene.free() first in after_each() destroys the node synchronously; GUT's autofree pass then runs against an already-invalid instance. The is_instance_valid() check prevents a crash, but this pattern is redundant and obscures intent. The same pattern appears in test_main_scene_parallax_chunks.gd and test_main_scene_performance_limits.gd.

Pick one strategy:

  • Keep autofree, drop manual free(): Let autofree own lifecycle; GUT will handle cleanup.
  • Drop autofree, keep manual free(): Use add_child(main_scene) instead and call free() explicitly if immediate teardown is required.
Option A: keep autofree, drop manual free
 func after_each() -> void:
-	if is_instance_valid(main_scene):
-		main_scene.free()
-	await get_tree().process_frame
+	# main_scene is released by add_child_autofree; flush a frame to settle deferred work.
+	await get_tree().process_frame
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@test/gut/test_decor_layer_transformations.gd` around lines 16 - 28, The
teardown currently redundantly calls main_scene.free() in after_each() even
though before_each() uses add_child_autofree(main_scene); remove the manual free
to let GUT's autofree handle lifecycle: delete the is_instance_valid(...) {
main_scene.free() } block from after_each() (leave the await
get_tree().process_frame) so add_child_autofree and main_scene remain the single
lifecycle mechanism; alternatively, if you prefer manual freeing, replace
add_child_autofree(main_scene) in before_each() with add_child(main_scene) and
keep the explicit free() in after_each()—pick one strategy and apply the change
consistently across test_decor_layer_transformations.gd,
test_main_scene_parallax_chunks.gd, and test_main_scene_performance_limits.gd.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@test/gut/test_main_scene_performance_limits.gd`:
- Around line 101-120: The test times main_scene._process but is flaky and
polluted by side effects: widen the threshold and/or increase the sample (e.g.,
>60 frames) or mark the test for a dedicated perf job, and eliminate the
first-call side effects by pre-warming or stubbing the branch that mutates
background.scroll_offset and calls show_message; specifically, call
main_scene._process once before timing (or set
Settings.has_unbound_critical_actions_for_current_device() to return false) to
avoid scheduling get_tree().create_timer and label updates, and ensure any
created timer/Label nodes are removed/cleaned up after the test so the
measurement only captures steady-state _process cost.

---

Outside diff comments:
In `@scripts/main_scene.gd`:
- Around line 194-211: The decor sprites are rotated around the top-left because
decor.centered = false, so applying allowed_rotations.pick_random() and setting
decor.rotation can push sprites outside the intended bounds when you clamp using
texture.get_width()/get_height() * scale_factor; fix this by setting the sprite
pivot to its center (set decor.centered = true) and adjust the position clamps
to account for half-extents (use (texture.get_width() * scale_factor) / 2 and
(texture.get_height() * scale_factor) / 2) before assigning decor.position.x and
decor.position.y so rotations around center stay within the layer.

---

Nitpick comments:
In `@test/gut/test_decor_layer_transformations.gd`:
- Around line 16-28: The teardown currently redundantly calls main_scene.free()
in after_each() even though before_each() uses add_child_autofree(main_scene);
remove the manual free to let GUT's autofree handle lifecycle: delete the
is_instance_valid(...) { main_scene.free() } block from after_each() (leave the
await get_tree().process_frame) so add_child_autofree and main_scene remain the
single lifecycle mechanism; alternatively, if you prefer manual freeing, replace
add_child_autofree(main_scene) in before_each() with add_child(main_scene) and
keep the explicit free() in after_each()—pick one strategy and apply the change
consistently across test_decor_layer_transformations.gd,
test_main_scene_parallax_chunks.gd, and test_main_scene_performance_limits.gd.

In `@test/gut/test_main_scene_parallax_chunks.gd`:
- Around line 70-102: The two near-duplicate tests
test_bushes_layer_chunk_size_and_density and
test_decor_layer_chunk_size_and_density should be consolidated by extracting the
common assertions into a single parameterized helper (or using gut's
parameterize_test) that accepts the layer setup method
(main_scene.setup_decor_layer / main_scene.setup_bushes_layer), the layer
accessor (main_scene.decor_layer / main_scene.bushes_layer) and the ID prefix
("decor_" / "bushes_"); re-run the appropriate setup via the passed setup
function, compute expected_height using viewport_mock.y * 8.0, get resource IDs
via texture_preloader.get_resource_list() filtered by the given prefix, compute
expected_count = ids.size() * 2, and assert the active child count (filtering
out is_queued_for_deletion) and motion_mirroring.y against those expected values
so both layer tests are covered by one parameterized test.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

Run ID: 16d625e6-6894-43c2-8d0c-39becf6e6220

📥 Commits

Reviewing files that changed from the base of the PR and between 8eb7436 and 909d4a1.

📒 Files selected for processing (7)
  • scripts/main_scene.gd
  • test/gut/test_decor_layer_transformations.gd
  • test/gut/test_decor_layer_transformations.gd.uid
  • test/gut/test_main_scene_parallax_chunks.gd
  • test/gut/test_main_scene_parallax_chunks.gd.uid
  • test/gut/test_main_scene_performance_limits.gd
  • test/gut/test_main_scene_performance_limits.gd.uid
✅ Files skipped from review due to trivial changes (3)
  • test/gut/test_main_scene_parallax_chunks.gd.uid
  • test/gut/test_decor_layer_transformations.gd.uid
  • test/gut/test_main_scene_performance_limits.gd.uid
📜 Review details
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: Browser Functional Tests / test
🧰 Additional context used
🧠 Learnings (1)
📚 Learning: 2026-03-30T04:02:23.747Z
Learnt from: ikostan
Repo: ikostan/SkyLockAssault PR: 500
File: test/gut/test_audio_web_bridge.gd:131-145
Timestamp: 2026-03-30T04:02:23.747Z
Learning: In GUT (Godot Unit Test) for Godot 4, when using `assert_called` / `assert_called_count` with parameter matching, include *every* argument the mocked method accepts, including parameters with default values. GUT does not auto-fill default arguments during call matching. For example, if `JavaScriptBridgeWrapper.eval(script: String, global_exec: bool = false)` is invoked as `eval(js_string)`, the actual call recorded by GUT includes the default (`eval(js_string, false)`), so your assertion must match both arguments (e.g., `.bind(js_string, false)`, not `.bind(js_string)`). Apply this rule to GUT assertions in `test/gut` tests.

Applied to files:

  • test/gut/test_decor_layer_transformations.gd
  • test/gut/test_main_scene_performance_limits.gd
  • test/gut/test_main_scene_parallax_chunks.gd
🔇 Additional comments (5)
scripts/main_scene.gd (1)

16-16: LGTM — concrete Panel type for stats_panel.

Replacing the implicit Variant annotation with Panel enables static checks on has_method("setup_hud") / setup_hud(player) call sites and improves editor tooling.

test/gut/test_decor_layer_transformations.gd (2)

34-65: LGTM — cardinal rotation assertion.

is_equal_approx against deg_to_rad({0, 90, 180, -90}) correctly accommodates the float representation of π/2 and π that Node2D.rotation returns via atan2, including the 180° boundary case.


102-119: This review comment is based on outdated code.

The test file has already been updated. Lines 102–138 now correctly validate that both true and false flip states appear across the decor sprites, using boolean tracking variables and proper assertions—exactly matching the suggested fix in this review comment. The tautological typeof() check no longer exists in the codebase.

			> Likely an incorrect or invalid review comment.
test/gut/test_main_scene_parallax_chunks.gd (1)

38-67: LGTM — chunk size and density assertions.

Computing expected_count from the actual texture_preloader resource list (rather than hardcoding a number) keeps the test resilient to content changes while still enforcing the 2x multiplier. motion_mirroring.y == viewport_mock.y * 8.0 is an exact product so assert_eq on floats is safe here.

test/gut/test_main_scene_performance_limits.gd (1)

34-95: LGTM — layer-height and density enforcement.

Checking motion_mirroring.y and 2x density on both layers guards against accidental tuning regressions. The inline comment calling out BVH/draw-call trade-offs is useful context for future maintainers.

Comment thread test/gut/test_main_scene_parallax_and_performance.gd
ikostan added 2 commits April 16, 2026 14:57
Rename test/gut/test_main_scene_performance_limits.gd -> test_main_scene_parallax_and_performance.gd (and corresponding .uid) and remove the separate test_main_scene_parallax_chunks.gd file. Add two new GUT tests that verify parallax layers: bushes and decor should mirror at exactly 8 screens tall (motion_mirroring.y == viewport.y * 8) and spawn 2x the number of sprites based on the texture preloader. Also add a brief initialization comment and ensure tests filter out nodes queued for deletion when counting active children.
Add a standardized safe_hard_free helper and use it across GUT tests to avoid double-free/orphan-window issues. Replace add_child_autofree with add_child and make after_each perform guarded teardown via safe_hard_free. Tighten and simplify test_main_scene_orphan_nodes.gd: explicit manual frees and nullification, added frame syncs, lambda type annotations, streamlined assertions, and cleanup of reloaded instances. These changes make test teardown deterministic and prevent orphan-node leaks and intermittent flakiness.
@ikostan
Copy link
Copy Markdown
Owner Author

ikostan commented Apr 16, 2026

  • Parallax chunk size and density behaviors (8-screen height and 2x density) are asserted in multiple suites (test_main_scene_parallax_chunks.gd, test_main_scene_performance_limits.gd, etc.); consolidating these checks into a single focused test file would reduce duplication and future maintenance overhead if those parameters change.
  • Test teardown strategies vary between files (some rely on autofree and frame flushes while others call free() explicitly); standardizing on a single, clearly documented cleanup pattern for MainScene tests would make lifecycle behavior easier to reason about and reduce the risk of subtle orphan-node issues reappearing.

Addressed

…ects leak into the measurement.

Two concerns with this test:

A hard < 1000 µs average over 60 script calls will occasionally fail on shared CI runners (GC pauses, other tests running concurrently, headless renderer under contention). A script-time performance bound this tight is better enforced as a benchmark trend rather than a pass/fail assertion. Consider widening the threshold substantially, running a larger sample, and/or tagging it so it only runs in a dedicated perf job.

main_scene._process on line 108 has side effects: it mutates background.scroll_offset, and on the first call will trigger show_message(...) if Settings.has_unbound_critical_actions_for_current_device() is true, which schedules a get_tree().create_timer(4.0) and adds a label update. The first iteration is therefore not representative, and the timer node lingers past the test. Worth either pre-warming once outside the timed loop, or explicitly setting state to skip the unbound branch during the measurement.
@ikostan
Copy link
Copy Markdown
Owner Author

ikostan commented Apr 16, 2026

@sourcery-ai review

Copy link
Copy Markdown
Contributor

@sourcery-ai sourcery-ai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hey - I've found 1 issue, and left some high level feedback:

  • Using child.free() in setup_bushes_layer/setup_decor_layer is more aggressive than queue_free() and can be risky if any of those sprites ever end up with external references or signals; consider whether queue_free() plus a frame flush or a dedicated cleanup pass would be safer for runtime code outside of tests.
  • The performance test test_process_script_execution_time depends on Time.get_ticks_usec() and absolute timing thresholds, which can be flaky across machines/CI; consider asserting more structural conditions (e.g. no allocations, no heavy loops) or using a much looser bound or profiler-based checks instead.
  • The safe_hard_free helper is duplicated across several GUT test files; consider centralizing this into a shared test utility script to keep teardown behavior consistent and easier to adjust in one place.
Prompt for AI Agents
Please address the comments from this code review:

## Overall Comments
- Using `child.free()` in `setup_bushes_layer`/`setup_decor_layer` is more aggressive than `queue_free()` and can be risky if any of those sprites ever end up with external references or signals; consider whether `queue_free()` plus a frame flush or a dedicated cleanup pass would be safer for runtime code outside of tests.
- The performance test `test_process_script_execution_time` depends on `Time.get_ticks_usec()` and absolute timing thresholds, which can be flaky across machines/CI; consider asserting more structural conditions (e.g. no allocations, no heavy loops) or using a much looser bound or profiler-based checks instead.
- The `safe_hard_free` helper is duplicated across several GUT test files; consider centralizing this into a shared test utility script to keep teardown behavior consistent and easier to adjust in one place.

## Individual Comments

### Comment 1
<location path="scripts/main_scene.gd" line_range="133" />
<code_context>
-	var layer_height: float = viewport.y * 4
+	# THE GOLDILOCKS ZONE:
+	# 8 screens is the sweet spot for infinite illusion vs CPU overhead
+	var screens_tall: float = 8.0
+	var layer_height: float = viewport.y * screens_tall
+
</code_context>
<issue_to_address>
**suggestion:** The hardcoded `screens_tall = 8.0` is duplicated and would be easier to tune as a single constant/config.

Since both `setup_bushes_layer` and `setup_decor_layer` use this `screens_tall = 8.0` value, any future adjustment would require changing it in multiple places. Please extract it into a shared constant or exported property so the parallax layer height is defined once and stays consistent across both layers.

Suggested implementation:

```
	# THE GOLDILOCKS ZONE:
	# 8 screens is the sweet spot for infinite illusion vs CPU overhead
	# Exposed so both bushes and decor parallax layers share the same height tuning.
	@export var parallax_screens_tall: float = 8.0

@onready var player: Node2D = $Player

```

In addition to the edit above, you should:
1. In `setup_bushes_layer`, replace the local hardcoded value:
   - Change: `var screens_tall: float = 8.0`
   - To: `var screens_tall: float = parallax_screens_tall`
2. In `setup_decor_layer`, do the same replacement:
   - Change: `var screens_tall: float = 8.0`
   - To: `var screens_tall: float = parallax_screens_tall`
3. If there are any other places in this script that use a hardcoded `8.0` specifically for parallax layer height (e.g. `viewport.y * 8.0`), update them to multiply by `parallax_screens_tall` instead so all layers stay consistent.
</issue_to_address>

Sourcery is free for open source - if you like our reviews please consider sharing them ✨
Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.

Comment thread scripts/main_scene.gd Outdated
@ikostan
Copy link
Copy Markdown
Owner Author

ikostan commented Apr 16, 2026

@sourcery-ai title

@sourcery-ai sourcery-ai Bot changed the title Orphan node leak from placeholder sprites in main scene Fix main scene orphan leaks and tune parallax decor with tests Apr 16, 2026
…be easier to tune as a single constant/config.

Since both setup_bushes_layer and setup_decor_layer use this screens_tall = 8.0 value, any future adjustment would require changing it in multiple places. Please extract it into a shared constant or exported property so the parallax layer height is defined once and stays consistent across both layers.
@ikostan
Copy link
Copy Markdown
Owner Author

ikostan commented Apr 16, 2026

  • Using child.free() in setup_bushes_layer/setup_decor_layer is more aggressive than queue_free() and can be risky if any of those sprites ever end up with external references or signals; consider whether queue_free() plus a frame flush or a dedicated cleanup pass would be safer for runtime code outside of tests.

The use of free() here is an intentional architectural choice to satisfy the requirements of Issue #540 (Eliminating the orphan window). Since these are procedurally generated Sprite2D assets with no external signals or persistent references, a hard free() after remove_child() is the most performant and memory-accurate way to reset the parallax layers without accumulating transient orphans.

ikostan added 3 commits April 16, 2026 15:37
Remove duplicate safe_hard_free implementations from test/gut/test_decor_layer_transformations.gd and test/gut/test_main_scene_orphan_nodes.gd, add a const GutHelper preload, and replace local calls with GutHelper.safe_hard_free(...). Centralizes the safe-free logic to avoid orphan windows/double-frees and reduces duplicated test code.
@ikostan
Copy link
Copy Markdown
Owner Author

ikostan commented Apr 16, 2026

  • The performance test test_process_script_execution_time depends on Time.get_ticks_usec() and absolute timing thresholds, which can be flaky across machines/CI; consider asserting more structural conditions (e.g. no allocations, no heavy loops) or using a much looser bound or profiler-based checks instead.
  • The safe_hard_free helper is duplicated across several GUT test files; consider centralizing this into a shared test utility script to keep teardown behavior consistent and easier to adjust in one place.

Both points have been addressed. Here is the breakdown of how the implementation now aligns with that feedback:

1. Centralized safe_hard_free Helper

The duplication issue is resolved by centralizing the logic into our shared utility script.

  • Centralization: The safe_hard_free function is now defined as a static method within gut_test_helper.gd.
  • Standardized Usage: All your test files—test_decor_layer_transformations.gd, test_main_scene_orphan_nodes.gd , and test_main_scene_parallax_and_performance.gd —now preload this helper and call GutHelper.safe_hard_free(main_scene) in their after_each blocks.
  • Robustness: The helper includes checks for is_instance_valid and is_queued_for_deletion, and explicitly detaches the node from the tree before calling free() to prevent dangling references.

2. Loosened Performance Timing Thresholds

The flakiness of the execution time test has been mitigated by adopting a more realistic and "loose" threshold for CI environments.

  • Loosened Bound: In test_main_scene_parallax_and_performance.gd, the threshold for average_time_per_frame_usec was increased to 2000.0 µs (2 ms).
  • CI Stability: The code comments explicitly state this was done to avoid flakiness while still being tight enough to catch major script regressions.
  • Pre-warming: The test now includes a "pre-warm" call to _process before starting the timer to ensure one-shot initialization logic (like showing initial messages) doesn't pollute the average timing.

Final Implementation Summary

Requirement Status Implementation Detail
Centralized Helper COMPLETED Moved to gut_test_helper.gd and preloaded in all suites.
Looser Timing Bound COMPLETED Increased to 2 ms in test_main_scene_parallax_and_performance.gd.
Type Safety COMPLETED All lambda filters now include explicit -> bool return types.
Parallax Constants COMPLETED Uses @export var parallax_screens_tall for consistent layer height.

@ikostan ikostan merged commit 82b0a4b into main Apr 16, 2026
12 checks passed
@ikostan ikostan deleted the orphan-node-leak-from-placeholder-sprites-in-main_scenegd branch April 16, 2026 22:59
@github-project-automation github-project-automation Bot moved this from In Progress to Done in Sky Lock Assault Project Apr 16, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

bug Something isn't working enhancement New feature or request good first issue Good for newcomers GUI testing

Projects

Status: Done

2 participants