OverviewFilesCommitsIssuesDocsReleasesPeers

nvidia-rtx-godot commits / 4bd871b

Classify invalid RID frees as double-free vs never-valid
backtrace_dump() produces nothing usable in these builds even with debug_symbols=yes, so two attempts at "where did this free come from" have gone nowhere. Ask a different question instead -- was the ID ever valid? -- which is what actually splits the candidates: DOUBLE FREE the ID was allocated and freed by this device before, so something is holding a stale handle past the resource's life and freeing it again. That is the RT surface cache hypothesis (RTSurfaceData keyed on mesh local_index with no invalidation hook, hyper-fleeky-dev #29). never valid the ID was never live here: uninitialised or corrupted memory, or a handle from elsewhere. A different bug entirely. _free_internal now records every ID it successfully frees and consults that history on the invalid path. The RID is also decoded -- RID_Alloc packs (index << 32) | validator -- so a recurring index with a changing validator is visible slot reuse, the signature of a stale handle surviving a free/realloc cycle. Debug builds only: the history grows with the number of freed resources, and this is a diagnostic rather than a correctness mechanism. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
4bd871b1f8eddb6e7bce6bec69f1ca1b56e89922
by plankatron <fleeky@gmail.com> · 2026-07-29T21:36:19.000Z

Changed files (2) +45 -2

servers/rendering/rendering_device.cpp +37 -2
……@@ -7728,6 +7728,12 @@ void RenderingDevice::free_rid(RID p_rid) {
77287728}
77297729
77307730void RenderingDevice::_free_internal(RID p_id) {
7731#ifdef DEBUG_ENABLED
7732 // Set only on the invalid-free path below, so the tail of this function can
7733 // record which IDs were genuinely freed. That history is what lets a later
7734 // invalid free be reported as a DOUBLE free rather than just "invalid".
7735 bool dbg_was_invalid = false;
7736#endif
77317737#ifdef DEV_ENABLED
77327738 String resource_name;
77337739 if (resource_names.has(p_id)) {
……@@ -7858,6 +7864,9 @@ void RenderingDevice::_free_internal(RID p_id) {
78587864 frames[frame].buffers_to_dispose_of.push_back(*hit_sbt);
78597865 hit_sbt_owner.free(p_id);
78607866 } else {
7867#ifdef DEBUG_ENABLED
7868 dbg_was_invalid = true;
7869#endif
78617870 static int trace_count = 0;
78627871 String trace_suffix;
78637872 if (trace_count < 20) {
……@@ -7875,11 +7884,31 @@ void RenderingDevice::_free_internal(RID p_id) {
78757884 static int invalid_free_count = 0;
78767885 invalid_free_count++;
78777886
7887 // backtrace_dump() yields nothing usable in these builds even with
7888 // debug_symbols, so instead of asking WHERE the free came from, answer the
7889 // question that actually splits the candidates: was this ID ever valid?
7890 //
7891 // "double free" -> it was allocated and freed by us before, so something
7892 // is holding a stale handle (e.g. a cache that outlived
7893 // the resource) and freeing it a second time.
7894 // "never valid" -> it was never a live RID here: uninitialised or
7895 // corrupted memory, or an ID from another RD instance.
7896 //
7897 // The RID is also decoded: RID_Alloc packs (index << 32) | validator, so a
7898 // recurring index with a changing validator is slot reuse -- the signature
7899 // of stale handles surviving a free/realloc cycle.
7900 String dbg_decoded;
7901#ifdef DEBUG_ENABLED
7902 String dbg_kind = _dbg_freed_ids.has(p_id.get_id()) ? "DOUBLE FREE (freed before)" : "never valid";
7903 dbg_decoded = " [index=" + itos(p_id.get_id() >> 32) +
7904 " validator=" + itos(p_id.get_id() & 0xFFFFFFFF) + " " + dbg_kind + "]";
7905#endif
7906
78787907 if (invalid_free_count <= PRINT_LIMIT) {
78797908#ifdef DEV_ENABLED
7880 ERR_PRINT("Attempted to free invalid ID: " + itos(p_id.get_id()) + " " + resource_name + trace_suffix);
7909 ERR_PRINT("Attempted to free invalid ID: " + itos(p_id.get_id()) + " " + resource_name + dbg_decoded + trace_suffix);
78817910#else
7882 ERR_PRINT("Attempted to free invalid ID: " + itos(p_id.get_id()) + trace_suffix);
7911 ERR_PRINT("Attempted to free invalid ID: " + itos(p_id.get_id()) + dbg_decoded + trace_suffix);
78837912#endif
78847913 if (invalid_free_count == PRINT_LIMIT) {
78857914 ERR_PRINT("Attempted to free invalid ID: reached " + itos(PRINT_LIMIT) +
……@@ -7890,6 +7919,12 @@ void RenderingDevice::_free_internal(RID p_id) {
78907919 }
78917920 }
78927921
7922#ifdef DEBUG_ENABLED
7923 if (!dbg_was_invalid) {
7924 _dbg_freed_ids.insert(p_id.get_id());
7925 }
7926#endif
7927
78937928 frames_pending_resources_for_processing = uint32_t(frames.size());
78947929}
78957930
servers/rendering/rendering_device.h +8 -0
……@@ -1912,6 +1912,14 @@ protected:
19121912
19131913public:
19141914 void _free_internal(RID p_id);
1915#ifdef DEBUG_ENABLED
1916 // Every RID this device has successfully freed. Used only to classify an
1917 // invalid free as a DOUBLE free (stale handle held past the resource's life)
1918 // versus an ID that was never valid here (uninitialised/corrupt). Debug-only:
1919 // it grows with the number of freed resources and is a diagnostic, not a
1920 // correctness mechanism.
1921 HashSet<uint64_t> _dbg_freed_ids;
1922#endif
19151923 void _begin_frame(bool p_presented = false);
19161924 void _end_frame();
19171925 void _execute_frame(bool p_present);