mirror of
https://github.com/libretro/parallel-ps2.git
synced 2026-11-09 17:28:25 -10:00
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:
1 parent
0358be6110
commit
3bf33f90e8
3 files changed
+108
-4
No files matched your search
@@ -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
|
||||
|
||||
Executable
+15
@@ -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"
|
||||
@@ -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;
|
||||
}
|
||||
Reference in new issue
Block a user