GS: an empty FastList owns no memory, and the suite pins what it does

FastList's clear(), which its constructor calls, allocated a four-element
buffer: one 64-byte-aligned allocation per list whether anything was ever
put in it. The texture cache keeps one list per GS page - GS_MAX_PAGES of
them, five hundred and twelve - and a frame touches a handful, so a
texture cache paid five hundred-odd allocations to hold nothing, and the
pages a frame does walk are spread across them.

It allocates on the first insertion instead, in an Allocate() that is
what clear() used to do. An empty list has no buffer, so the three reads
that went through m_buffer[0] answer without one: index 0 is both the
auxiliary element and "past the end", which is what an empty list's
first and last already were. size(), empty(), begin(), end(), rbegin()
and rend() are unchanged in what they return.

tests/fastlist is new and run-all.sh picks it up: insertion order, both
iteration directions, back/pop_back, erase through an iterator,
MoveFront, reuse after clear, and the same on a list that has never held
anything. It passes against the version before this change as well -
that is the point of it - and plain and under ASan after.

GSTextureCache.cpp, GSDevice.cpp and GSState.cpp compile with the
Makefile's flags, GSRendererSW.cpp with its multi-ISA ones. Not run in
the emulator.
This commit is contained in:
LibretroAdmin authored and libretroadmin committed 2026-09-20 00:57:22 +02:00
1 parent 0358be6110
commit 3bf33f90e8
3 files changed
+108 -4

No files matched your search

+28 -4
View File
@@ -70,6 +70,25 @@ public:
}
void clear()
{
// An empty list owns nothing. It used to allocate its four-element
// buffer here, which is a 64-byte-aligned allocation per list
// whether or not anything is ever put in it -- and the texture
// cache keeps one list per GS page, GS_MAX_PAGES of them, of which
// a frame touches a handful. Allocate on the first insertion
// instead; everything that reads an empty list is answered below
// without touching the buffer.
memalign_free(m_buffer);
m_buffer = nullptr;
m_free_indexes_stack = nullptr;
m_capacity = 0;
m_free_indexes_stack_top = 0;
}
private:
// The first insertion into an empty list: the allocation clear() used
// to do, with the same initial capacity.
void Allocate()
{
// Initialize m_capacity to 4 so we avoid to Grow() on initial insertions
// The code doesn't break if this value is changed with anything from 1 to USHRT_MAX
@@ -77,7 +96,6 @@ public:
// Initialize m_buffer and m_free_indexes_stack as a contiguous block of memory starting at m_buffer
// This should increase cache locality and reduce memory fragmentation
memalign_free(m_buffer);
m_buffer = (Element<T>*)memalign_alloc(64, m_capacity * sizeof(Element<T>) + (m_capacity - 1) * sizeof(u16));
m_free_indexes_stack = (u16*)&m_buffer[m_capacity];
@@ -92,10 +110,14 @@ public:
m_free_indexes_stack[i] = i + 1;
}
public:
// Insert the element in front of the list and return its position in m_buffer
__forceinline u16 InsertFront(const T& data)
{
if (Full())
if (!m_buffer)
Allocate();
else if (Full())
Grow();
// Pop a free index from the stack
@@ -188,14 +210,16 @@ private:
return m_buffer[index].prev_index;
}
/* Index 0 is the auxiliary element and also "past the end", so an
* unallocated list answers 0 to both without a buffer to read. */
__forceinline u16 FirstIndex() const
{
return m_buffer[0].next_index;
return m_buffer ? m_buffer[0].next_index : 0;
}
__forceinline u16 LastIndex() const
{
return m_buffer[0].prev_index;
return m_buffer ? m_buffer[0].prev_index : 0;
}
__forceinline bool Full() const
+15
View File
@@ -0,0 +1,15 @@
#!/bin/sh
# FastList (pcsx2/GS/Renderers/Common/GSFastList.h): the intrusive list the
# texture cache keeps one of per GS page. It allocates on the first
# insertion rather than at construction, so the empty case is the one
# worth pinning - an unallocated list must size, compare, and iterate like
# any other empty one.
set -e
DIR=$(CDPATH= cd -- "$(dirname -- "$0")" && pwd)
ROOT=$(CDPATH= cd -- "$DIR/../.." && pwd)
LC="$ROOT/libretro/libretro-common"
INC="-I $ROOT -I $ROOT/pcsx2 -I $ROOT/common -I $ROOT/common/include -I $LC/include"
SRC="$DIR/main.cpp $LC/memmap/memalign.c"
${CXX:-c++} -std=c++17 -O2 -g -w $SANFLAGS $INC -o "$DIR/fastlist_test" $SRC
"$DIR/fastlist_test"
+65
View File
@@ -0,0 +1,65 @@
/* FastList now allocates on the first insertion rather than in clear().
* Everything it does must be unchanged, including on a list that has
* never held anything. */
#include <cstdio>
#include <vector>
#include <algorithm>
#include "common/Pcsx2Defs.h"
#include "GS/Renderers/Common/GSFastList.h"
static int fails;
#define CHECK(c, m) do { if (!(c)) { printf(" FAIL: %s\n", m); fails++; } } while (0)
int main()
{
{ /* never touched */
FastList<int> l;
CHECK(l.size() == 0, "fresh: size 0");
CHECK(l.empty(), "fresh: empty");
CHECK(l.begin() == l.end(), "fresh: begin == end");
int n = 0;
for (auto it = l.begin(); it != l.end(); ++it) n++;
CHECK(n == 0, "fresh: iterates nothing");
n = 0;
for (auto it = l.rbegin(); it != l.rend(); ++it) n++;
CHECK(n == 0, "fresh: reverse-iterates nothing");
l.clear();
CHECK(l.empty(), "cleared while empty: still empty");
}
{ /* insert, read back, erase, and again after a clear */
FastList<int> l;
for (int pass = 0; pass < 3; pass++)
{
std::vector<int> want;
for (int i = 0; i < 300; i++) { l.push_front(i); want.push_back(i); }
std::reverse(want.begin(), want.end());
CHECK(l.size() == 300, "300 inserted: size");
std::vector<int> got;
for (auto it = l.begin(); it != l.end(); ++it) got.push_back(*it);
CHECK(got == want, "front insertion order preserved");
std::vector<int> rgot;
for (auto it = l.rbegin(); it != l.rend(); ++it) rgot.push_back(*it);
std::reverse(rgot.begin(), rgot.end());
CHECK(rgot == want, "reverse iteration is the mirror");
CHECK(l.back() == 0, "back is the oldest");
l.pop_back();
CHECK(l.size() == 299, "pop_back: size");
for (auto it = l.begin(); it != l.end();)
it = (*it % 2) ? l.erase(it) : ++it;
CHECK(l.size() == 149, "erase every odd: size (1..299 has 150 odds)");
for (auto it = l.begin(); it != l.end(); ++it)
CHECK(*it % 2 == 0, "only evens remain");
l.clear();
CHECK(l.size() == 0 && l.begin() == l.end(), "clear empties it");
}
}
{ /* MoveFront, which walks the chain through index 0 */
FastList<int> l;
u16 idx[8];
for (int i = 0; i < 8; i++) idx[i] = l.InsertFront(i);
l.MoveFront(idx[0]);
CHECK(*l.begin() == 0, "MoveFront brings it to the front");
l.MoveFront(idx[0]);
CHECK(*l.begin() == 0, "MoveFront of the front is a no-op");
}
printf(fails ? "fastlist: FAILED (%d)\n" : "fastlist: ok\n", fails);
return fails != 0;
}