From 71bf02d77b2c38cd99e875ab92daa20b94fb777a Mon Sep 17 00:00:00 2001 From: Chris Speciale <26172437+dimensionscape@users.noreply.github.com> Date: Fri, 2 Oct 2026 18:46:32 -0400 Subject: [PATCH 1/5] Fix block row counts overflowing with HXCPP_GC_BIG_BLOCKS #1402 made BlockDataInfo's mUsedRows and mFraggedRows uint8_t, which fits the default 32k blocks (254 usable lines) but not big blocks (508). With HXCPP_GC_BIG_BLOCKS the counts wrap, the GC underestimates the live heap, and it ends up collecting on almost every new block. Use uint16_t for both (and for the allocator's mFraggedRows pointer), with static_asserts so they can't drop below IMMIX_USEFUL_LINES again. --- src/hx/gc/Immix.cpp | 18 +++++++++++++----- 1 file changed, 13 insertions(+), 5 deletions(-) diff --git a/src/hx/gc/Immix.cpp b/src/hx/gc/Immix.cpp index 9eb91603e..3310995ec 100644 --- a/src/hx/gc/Immix.cpp +++ b/src/hx/gc/Immix.cpp @@ -12,6 +12,7 @@ #include #include #include +#include #ifdef __EMSCRIPTEN__ #include @@ -754,11 +755,11 @@ struct BlockDataInfo HoleRange mRanges[MAX_HOLES]; uint8_t mHoles; - uint8_t mUsedRows; + uint16_t mUsedRows; uint16_t mMaxHoleSize; int mMoveScore; uint16_t mUsedBytes; - uint8_t mFraggedRows; + uint16_t mFraggedRows; bool mPinned; unsigned char mZeroed; bool mReclaimed; @@ -1418,6 +1419,13 @@ struct BlockDataInfo #endif }; +// A block's row counts run up to IMMIX_USEFUL_LINES: 254 with the default 32k blocks, but 508 with +// HXCPP_GC_BIG_BLOCKS' 64k blocks, more than a uint8_t holds. +static_assert(IMMIX_USEFUL_LINES <= std::numeric_limits::max(), + "BlockDataInfo::mUsedRows must hold IMMIX_USEFUL_LINES"); +static_assert(IMMIX_USEFUL_LINES <= std::numeric_limits::max(), + "BlockDataInfo::mFraggedRows must hold IMMIX_USEFUL_LINES"); + static bool SmallestFreeFirst(BlockDataInfo *inA, BlockDataInfo *inB) { return inA->mMaxHoleSize < inB->mMaxHoleSize; @@ -5768,7 +5776,7 @@ class LocalAllocator : public hx::StackContext uint8_t mCurrentHole; uint8_t mCurrentHoles; HoleRange *mCurrentRange; - uint8_t *mFraggedRows; + uint16_t *mFraggedRows; bool mMoreHoles; @@ -6259,7 +6267,7 @@ class LocalAllocator : public hx::StackContext // spaceOversize might have been set to zero for quick-termination of alloc. unsigned char* s{ spaceOversize }; if (s>spaceFirst && mFraggedRows) - *mFraggedRows += static_cast((s - spaceFirst) >> IMMIX_LINE_BITS); + *mFraggedRows += static_cast((s - spaceFirst) >> IMMIX_LINE_BITS); #else #ifdef HXCPP_ALIGN_ALLOC if (!(size_t{ spaceStart } & 0x4)) @@ -6296,7 +6304,7 @@ class LocalAllocator : public hx::StackContext } if (mFraggedRows && spaceEnd > spaceStart) { - *mFraggedRows += static_cast((spaceEnd - spaceStart) >> IMMIX_LINE_BITS); + *mFraggedRows += static_cast((spaceEnd - spaceStart) >> IMMIX_LINE_BITS); } #endif From 13f81b3d3ec5ce9986649d75910f692aa100b393 Mon Sep 17 00:00:00 2001 From: Chris Speciale <26172437+dimensionscape@users.noreply.github.com> Date: Fri, 9 Oct 2026 11:35:13 -0400 Subject: [PATCH 2/5] Check block row counts against IMMIX_USEFUL_LINES in debug builds getStats(), countRows() and reclaim() add each block's row counts into the heap stats that size working memory. In debug builds, trap there if mUsedRows or mFraggedRows is above the block's useful lines, so a corrupted count is caught before it skews the stats. --- src/hx/gc/Immix.cpp | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/src/hx/gc/Immix.cpp b/src/hx/gc/Immix.cpp index 3310995ec..2a383eda3 100644 --- a/src/hx/gc/Immix.cpp +++ b/src/hx/gc/Immix.cpp @@ -899,8 +899,22 @@ struct BlockDataInfo bool isEmpty() const { return mUsedRows == 0; } int getUsedRows() const { return mUsedRows; } + // A row count above the block's useful lines is corrupted GC state, about to be + // added into the heap stats that size working memory. + void checkRowCounts() const + { + #ifdef HXCPP_DEBUG + if (mUsedRows>IMMIX_USEFUL_LINES || mFraggedRows>IMMIX_USEFUL_LINES) + { + GCLOG("Bad block row counts: used %d, fragged %d, max %d\n", (int)mUsedRows, (int)mFraggedRows, (int)IMMIX_USEFUL_LINES); + DebuggerTrap(); + } + #endif + } + void getStats(BlockDataStats &outStats) { + checkRowCounts(); outStats.rowsInUse += mUsedRows; outStats.bytesInUse += mUsedBytes; outStats.fraggedRows += mFraggedRows; @@ -981,6 +995,7 @@ struct BlockDataInfo mZeroLock = 0; mOwned = false; + checkRowCounts(); outStats.rowsInUse += mUsedRows; outStats.bytesInUse += mUsedBytes; outStats.fraggedRows += mFraggedRows; @@ -1150,6 +1165,7 @@ struct BlockDataInfo mMoveScore = calcFragScore(); mReclaimed = true; + checkRowCounts(); if (outStats) { outStats->rowsInUse += mUsedRows; From 1018969ba863dab63a8e1fab4bfc8a63fd4f1bcd Mon Sep 17 00:00:00 2001 From: Chris Speciale <26172437+dimensionscape@users.noreply.github.com> Date: Fri, 9 Oct 2026 11:48:09 -0400 Subject: [PATCH 3/5] Remove the comment above the row count static_asserts --- src/hx/gc/Immix.cpp | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/hx/gc/Immix.cpp b/src/hx/gc/Immix.cpp index 2a383eda3..bed55a9c8 100644 --- a/src/hx/gc/Immix.cpp +++ b/src/hx/gc/Immix.cpp @@ -1435,8 +1435,6 @@ struct BlockDataInfo #endif }; -// A block's row counts run up to IMMIX_USEFUL_LINES: 254 with the default 32k blocks, but 508 with -// HXCPP_GC_BIG_BLOCKS' 64k blocks, more than a uint8_t holds. static_assert(IMMIX_USEFUL_LINES <= std::numeric_limits::max(), "BlockDataInfo::mUsedRows must hold IMMIX_USEFUL_LINES"); static_assert(IMMIX_USEFUL_LINES <= std::numeric_limits::max(), From 3138b2594dc5bfc8e1aec27ba0ffaa6b427319d8 Mon Sep 17 00:00:00 2001 From: Chris Speciale <26172437+dimensionscape@users.noreply.github.com> Date: Fri, 9 Oct 2026 11:48:09 -0400 Subject: [PATCH 4/5] [ci] Build the haxe tests with HXCPP_GC_BIG_BLOCKS Nothing in CI builds big blocks, so a change that breaks it goes unnoticed. This is a debug build, so the block row count checks run as well. --- .github/workflows/test.yml | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 3eac4e98d..d303e9b82 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -40,6 +40,24 @@ jobs: - name: run run: bin${{ inputs.sep }}TestMain + big_blocks: + runs-on: ${{ inputs.os }} + name: haxe (big blocks) + defaults: + run: + working-directory: test/haxe + steps: + - name: checkout + uses: actions/checkout@v4 + - name: setup + uses: ./.github/workflows/setup + with: + haxe: ${{ inputs.haxe }} + - name: build + run: haxe compile.hxml --debug -D HXCPP_GC_BIG_BLOCKS -D ${{ env.HXCPP_ARCH_FLAG }} + - name: run + run: bin${{ inputs.sep }}TestMain-debug + cffi: runs-on: ${{ inputs.os }} name: cffi From 09fb2c5f87fcd860c9eaef1d0bba7b0131cff9d8 Mon Sep 17 00:00:00 2001 From: Chris Speciale <26172437+dimensionscape@users.noreply.github.com> Date: Fri, 9 Oct 2026 11:54:47 -0400 Subject: [PATCH 5/5] Widen IMMIX_BLOCK_SIZE and IMMIX_USEFUL_LINES to fit big blocks With HXCPP_GC_BIG_BLOCKS, IMMIX_BLOCK_SIZE is 65536 and IMMIX_USEFUL_LINES is 508, too big for the uint16_t and uint8_t they were sized as, so their brace initialisers fail to compile. Default builds compute the same values: IMMIX_USEFUL_LINES still promotes to int, and every use of IMMIX_BLOCK_SIZE is size_t or non-negative arithmetic. --- src/hx/gc/Immix.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/hx/gc/Immix.cpp b/src/hx/gc/Immix.cpp index e0ca2d6dc..7de470b6b 100644 --- a/src/hx/gc/Immix.cpp +++ b/src/hx/gc/Immix.cpp @@ -495,13 +495,13 @@ extern void scriptMarkStack(hx::MarkContext *); using BlockIdType = unsigned short; #endif -constexpr uint16_t IMMIX_BLOCK_SIZE{ 1 << IMMIX_BLOCK_BITS }; +constexpr uint32_t IMMIX_BLOCK_SIZE{ 1 << IMMIX_BLOCK_BITS }; constexpr uint16_t IMMIX_BLOCK_OFFSET_MASK{ IMMIX_BLOCK_SIZE - 1 }; constexpr uint8_t IMMIX_LINE_COUNT_BITS{ IMMIX_BLOCK_BITS - IMMIX_LINE_BITS }; constexpr uint16_t IMMIX_LINES{ 1 << IMMIX_LINE_COUNT_BITS }; constexpr uintptr_t IMMIX_BLOCK_BASE_MASK{ ~uintptr_t{ IMMIX_BLOCK_OFFSET_MASK } }; constexpr uint8_t IMMIX_HEADER_LINES{ IMMIX_LINES >> IMMIX_LINE_BITS }; -constexpr uint8_t IMMIX_USEFUL_LINES{ IMMIX_LINES - IMMIX_HEADER_LINES }; +constexpr uint16_t IMMIX_USEFUL_LINES{ IMMIX_LINES - IMMIX_HEADER_LINES }; constexpr uint8_t IMMIX_MAX_ALLOC_GROUPS_SIZE{ 1 << IMMIX_BLOCK_GROUP_BITS }; // Every second line used