mirror of
https://github.com/hrydgard/ppsspp.git
synced 2026-09-01 02:05:21 +02:00
PointerWrap and the Do() overloads around it are how every savestate is
written and read, and had no direct coverage. Everything read back came off
disk, so the corrupt-input paths matter as much as the round trips.
Three bugs, all in the bounds checking added in 58d4759ceb:
1. sizeof(T) is not a lower bound on how many bytes an element serializes to.
It only holds for the types DoHelper_ writes out raw. A std::string is 32-40
bytes in memory and serializes to as few as five; a T* serializes to whatever
T::DoState() writes. So DoVector/DoList/DoSet/DoMap could reject a perfectly
valid savestate whenever count * sizeof(element) exceeded the bytes left in
the buffer. That is not hypothetical: pspFileSystem is serialized dead last
in SaveStart::DoState, and MetaFileSystem::DoState does Do(p, currentDir) on
a std::map<int, std::string>, so the check runs with only a few hundred bytes
remaining and claims 44 bytes per entry against roughly 22 actual. Added
SerializeMinElemSize<T>(), mirroring DoHelper_'s own condition, and used it
in all five containers. The bound is only loosened, so nothing that loaded
before can stop loading.
2. Do(p, std::map<K, T *> &) deletes every value before reading the new ones,
and DoMap then returned on a bad count without clearing - leaving the map
full of freed pointers to be used or deleted again. Six live maps go through
this (sceMpeg, sceMp3, sceAac, sceFont, sceHeap, sceKernelThread's pending
calls), so a corrupt savestate meant a use-after-free. Clear before the guard
can bail out, in DoMap, DoMultimap and DoSet.
3. The wstring and u16string overloads validated stringLen < 0 but not 0, and
didn't require a whole number of characters. read() computes
stringLen / sizeof(char) - 1, so a length of 0 resized to SIZE_MAX and
memcpy'd with a wrapped-around size. PSPOskDialog::DoState serializes both
(inputChars at v2, a legacy wstring below that), so this was reachable: the
test aborts the process without the fix.
The test covers round trips of PODs, strings (empty, embedded NUL), vector,
map, set, list and map-of-pointers, section titles and version gating in both
directions, marker mismatches, measure-vs-write checkpoint disagreement, the
error latch dropping to MODE_NOOP, every truncation of a valid buffer, and
hand-corrupted counts and lengths.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GZq8ZtJmFY7bkX5FVkr3P9
2996 lines
100 KiB
C++
2996 lines
100 KiB
C++
// Copyright (c) 2012- PPSSPP Project.
|
|
|
|
// This program is free software: you can redistribute it and/or modify
|
|
// it under the terms of the GNU General Public License as published by
|
|
// the Free Software Foundation, version 2.0 or later versions.
|
|
|
|
// This program is distributed in the hope that it will be useful,
|
|
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
// GNU General Public License 2.0 for more details.
|
|
|
|
// A copy of the GPL 2.0 should have been included with the program.
|
|
// If not, see http://www.gnu.org/licenses/
|
|
|
|
// Official git repository and contact information can be found at
|
|
// https://github.com/hrydgard/ppsspp and http://www.ppsspp.org/.
|
|
|
|
// UnitTests
|
|
//
|
|
// This is a program to directly test various functions, without going
|
|
// through a PSP. Especially useful for things like opcode emitters,
|
|
// hashes, and various data conversion utility function.
|
|
//
|
|
// TODO: Make a test of nice unittest asserts and count successes etc.
|
|
// Or just integrate with an existing testing framework.
|
|
//
|
|
// To use, set command line parameter to one or more of the tests below, or "all".
|
|
// Search for "availableTests".
|
|
//
|
|
// Example of how to run with CMake:
|
|
//
|
|
// ./b.sh --unittest
|
|
// build/unittest EscapeMenuString
|
|
|
|
#include "ppsspp_config.h"
|
|
|
|
#include <typeinfo>
|
|
|
|
#include <algorithm>
|
|
#include <cstdio>
|
|
#include <cstdlib>
|
|
#include <cmath>
|
|
#include <vector>
|
|
#include <string>
|
|
#include <sstream>
|
|
#include <unordered_map>
|
|
|
|
#if PPSSPP_PLATFORM(ANDROID)
|
|
#include <jni.h>
|
|
#endif
|
|
|
|
#include "Common/Data/Collections/TinySet.h"
|
|
#include "Common/Data/Collections/FastVec.h"
|
|
#include "Common/Data/Collections/CharQueue.h"
|
|
#include "Common/Data/Convert/SmallDataConvert.h"
|
|
#include "Common/Data/Text/Parsers.h"
|
|
#include "Common/Data/Text/WrapText.h"
|
|
#include "Common/Data/Encoding/Utf8.h"
|
|
#include "Common/Buffer.h"
|
|
#include "Common/File/Path.h"
|
|
#include "Common/Log/LogManager.h"
|
|
#include "Common/Math/SIMDHeaders.h"
|
|
#include "Common/Math/CrossSIMD.h"
|
|
// Get some more instructions for testing
|
|
#if PPSSPP_ARCH(SSE2)
|
|
#include <immintrin.h>
|
|
#endif
|
|
|
|
#include "Common/Input/InputState.h"
|
|
#include "Common/Math/math_util.h"
|
|
#include "Common/Render/DrawBuffer.h"
|
|
#include "Common/System/NativeApp.h"
|
|
#include "Common/System/System.h"
|
|
#include "Common/Thread/ThreadUtil.h"
|
|
#include "Common/Data/Format/IniFile.h"
|
|
#include "Common/TimeUtil.h"
|
|
|
|
#include "Common/ArmEmitter.h"
|
|
#include "Common/BitScan.h"
|
|
#include "Common/CPUDetect.h"
|
|
#include "Common/ExceptionHandlerSetup.h"
|
|
#include "Common/Log.h"
|
|
#include "Common/StringUtils.h"
|
|
#include "Core/Config.h"
|
|
#include "Common/Data/Convert/ColorConv.h"
|
|
#include "Common/File/VFS/VFS.h"
|
|
#include "Common/File/VFS/DirectoryReader.h"
|
|
#include "Common/Math/fast/fast_matrix.h"
|
|
#include "Common/Serialize/Serializer.h"
|
|
#include "Common/Serialize/SerializeFuncs.h"
|
|
#include "Common/Serialize/SerializeMap.h"
|
|
#include "Common/Serialize/SerializeSet.h"
|
|
#include "Common/Serialize/SerializeList.h"
|
|
#include <map>
|
|
#include <set>
|
|
#include <list>
|
|
#include "Core/CmdLine.h"
|
|
#include "Common/Data/Collections/Hashmaps.h"
|
|
#include "Core/Util/BlockAllocator.h"
|
|
#include "Core/Debugger/Breakpoints.h"
|
|
#include "Core/Debugger/SymbolMap.h"
|
|
#include "Core/Debugger/MemBlockInfo.h"
|
|
#include "Core/FileSystems/ISOFileSystem.h"
|
|
#include "Core/MemMap.h"
|
|
#include "Core/KeyMap.h"
|
|
#include "Core/Util/PathUtil.h"
|
|
#include "Core/MIPS/MIPSVFPUUtils.h"
|
|
#include "GPU/Common/TextureDecoder.h"
|
|
#include "GPU/Common/GPUStateUtils.h"
|
|
#include "GPU/Math3D.h"
|
|
|
|
#include "Common/File/AndroidContentURI.h"
|
|
|
|
#include "unittest/JitHarness.h"
|
|
#include "unittest/TestVertexJit.h"
|
|
#include "unittest/UnitTest.h"
|
|
|
|
// Set to true for more verbose unit tests.
|
|
bool g_testLog = false;
|
|
|
|
std::string System_GetProperty(SystemProperty prop) { return ""; }
|
|
std::vector<std::string> System_GetPropertyStringVec(SystemProperty prop) { return std::vector<std::string>(); }
|
|
int64_t System_GetPropertyInt(SystemProperty prop) {
|
|
return -1;
|
|
}
|
|
float System_GetPropertyFloat(SystemProperty prop) {
|
|
return -1;
|
|
}
|
|
bool System_GetPropertyBool(SystemProperty prop) {
|
|
switch (prop) {
|
|
case SYSPROP_CAN_JIT:
|
|
return true;
|
|
default:
|
|
return false;
|
|
}
|
|
}
|
|
void System_Notify(SystemNotification notification) {}
|
|
void System_PostUIMessage(UIMessage message, std::string_view param) {}
|
|
void System_RunOnMainThread(std::function<void()>) {}
|
|
void System_AudioGetDebugStats(char *buf, size_t bufSize) { if (buf) buf[0] = '\0'; }
|
|
void System_AudioClear() {}
|
|
void System_AudioPushSamples(const s32 *audio, int numSamples, float volume) {}
|
|
std::vector<std::string> System_GetCameraDeviceList() { return std::vector<std::string>(); }
|
|
|
|
// Temporary hacks around annoying linking errors. Copied from Headless.
|
|
void NativeFrame(GraphicsContext *graphicsContext) {}
|
|
void NativeResized() {}
|
|
|
|
bool System_MakeRequest(SystemRequestType type, int requestId, const std::string ¶m1, const std::string ¶m2, int64_t param3, int64_t param4) { return false; }
|
|
// Pulled in via Core/WebServer.cpp's OpenWebDebugger(), which CmdLine.cpp now references.
|
|
void System_LaunchUrl(LaunchUrlType urlType, std::string_view url) {}
|
|
void System_InputBoxGetString(const std::string &title, const std::string &defaultValue, std::function<void(bool, const std::string &)> cb) { cb(false, ""); }
|
|
void System_AskForPermission(SystemPermission permission) {}
|
|
PermissionStatus System_GetPermissionStatus(SystemPermission permission) { return PERMISSION_STATUS_GRANTED; }
|
|
|
|
// TODO: To avoid having to define these here, these should probably be turned into system "requests".
|
|
// To clear the secret entirely, just save an empty string.
|
|
bool NativeSaveSecret(std::string_view nameOfSecret, std::string_view data) { return false; }
|
|
std::string NativeLoadSecret(std::string_view nameOfSecret) { return ""; }
|
|
|
|
#if PPSSPP_PLATFORM(ANDROID)
|
|
JNIEnv *getEnv() {
|
|
return nullptr;
|
|
}
|
|
|
|
jclass findClass(const char *name) {
|
|
return nullptr;
|
|
}
|
|
|
|
bool System_AudioRecordingIsAvailable() { return false; }
|
|
bool System_AudioRecordingState() { return false; }
|
|
#endif
|
|
|
|
#ifndef M_PI_2
|
|
#define M_PI_2 1.57079632679489661923
|
|
#endif
|
|
|
|
// asin acos atan: https://github.com/michaldrobot/ShaderFastLibs/blob/master/ShaderFastMathLib.h
|
|
|
|
// TODO:
|
|
// Fast approximate sincos for NEON
|
|
// http://blog.julien.cayzac.name/2009/12/fast-sinecosine-for-armv7neon.html
|
|
// Fast sincos
|
|
// http://www.dspguru.com/dsp/tricks/parabolic-approximation-of-sin-and-cos
|
|
|
|
// minimax (surprisingly terrible! something must be wrong)
|
|
// double asin_plus_sqrtthing = .9998421793 + (1.012386649 + (-.6575341673 + .8999841642 + (-1.669668977 + (1.571945105 - .5860008052 * x) * x) * x) * x) * x;
|
|
|
|
// VERY good. 6 MAD, one division.
|
|
// double asin_plus_sqrtthing = (1.807607311 + (.191900116 + (-2.511278506 + (1.062519236 + (-.3572142480 + .1087063463 * x) * x) * x) * x) * x) / (1.807601897 - 1.615203794 * x);
|
|
// float asin_plus_sqrtthing_correct_ends =
|
|
// (1.807607311f + (.191900116f + (-2.511278506f + (1.062519236f + (-.3572142480f + .1087063463f * x) * x) * x) * x) * x) / (1.807607311f - 1.615195094 * x);
|
|
|
|
// Unfortunately this is very serial.
|
|
// At least there are only 8 constants needed - load them into two low quads and go to town.
|
|
// For every step, VDUP the constant into a new register (out of two alternating), then VMLA or VFMA into it.
|
|
|
|
// http://www.ecse.rpi.edu/~wrf/Research/Short_Notes/arcsin/
|
|
// minimax polynomial rational approx, pretty good, get four digits consistently.
|
|
// unfortunately fastasin(1.0) / M_PI_2 != 1.0f, but it's pretty close.
|
|
float fastasin(double x) {
|
|
float sign = x >= 0.0f ? 1.0f : -1.0f;
|
|
x = fabs(x);
|
|
float sqrtthing = sqrt(1.0f - x * x);
|
|
// note that the sqrt can run parallel while we do the rest
|
|
// if the hardware supports it
|
|
|
|
float y = -.3572142480f + .1087063463f * x;
|
|
y = y * x + 1.062519236f;
|
|
y = y * x + -2.511278506f;
|
|
y = y * x + .191900116f;
|
|
y = y * x + 1.807607311f;
|
|
y /= (1.807607311f - 1.615195094 * x);
|
|
return sign * (y - sqrtthing);
|
|
}
|
|
|
|
double atan_66s(double x) {
|
|
const double c1=1.6867629106;
|
|
const double c2=0.4378497304;
|
|
const double c3=1.6867633134;
|
|
|
|
double x2; // The input argument squared
|
|
|
|
x2 = x * x;
|
|
return (x*(c1 + x2*c2)/(c3 + x2));
|
|
}
|
|
|
|
// Terrible.
|
|
double fastasin2(double x) {
|
|
return atan_66s(x / sqrt(1 - x * x));
|
|
}
|
|
|
|
// Also terrible.
|
|
float fastasin3(float x) {
|
|
return x + x * x * x * x * x * 0.4971;
|
|
}
|
|
|
|
// Great! This is the one we'll use. Can be easily rescaled to get the right range for free.
|
|
// http://mathforum.org/library/drmath/view/54137.html
|
|
// http://www.musicdsp.org/showone.php?id=115
|
|
float fastasin4(float x) {
|
|
float sign = x >= 0.0f ? 1.0f : -1.0f;
|
|
x = fabs(x);
|
|
x = M_PI/2 - sqrtf(1.0f - x) * (1.5707288 + -0.2121144*x + 0.0742610*x*x + -0.0187293*x*x*x);
|
|
return sign * x;
|
|
}
|
|
|
|
// Or this:
|
|
float fastasin5(float x)
|
|
{
|
|
float sign = x >= 0.0f ? 1.0f : -1.0f;
|
|
x = fabs(x);
|
|
float fRoot = sqrtf(1.0f - x);
|
|
float fResult = 0.0742610f + -0.0187293f * x;
|
|
fResult = -0.2121144f + fResult * x;
|
|
fResult = 1.5707288f + fResult * x;
|
|
fResult = M_PI/2 - fRoot*fResult;
|
|
return sign * fResult;
|
|
}
|
|
|
|
|
|
// This one is unfortunately not very good. But lets us avoid PI entirely
|
|
// thanks to the special arguments of the PSP functions.
|
|
// http://www.dspguru.com/dsp/tricks/parabolic-approximation-of-sin-and-cos
|
|
#define C 0.70710678118654752440f // 1.0f / sqrt(2.0f)
|
|
// Some useful constants (PI and <math.h> are not part of algo)
|
|
#define BITSPERQUARTER (20)
|
|
void fcs(float angle, float &sinout, float &cosout) {
|
|
int phasein = angle * (1 << BITSPERQUARTER);
|
|
// Modulo phase into quarter, convert to float 0..1
|
|
float modphase = (phasein & ((1<<BITSPERQUARTER)-1)) * (1.0f / (1<<BITSPERQUARTER));
|
|
// Extract quarter bits
|
|
int quarter = phasein >> BITSPERQUARTER;
|
|
// Recognize quarter
|
|
if (!quarter) {
|
|
// First quarter, angle = 0 .. pi/2
|
|
float x = modphase - 0.5f; // 1 sub
|
|
float temp = (2 - 4*C)*x*x + C; // 2 mul, 1 add
|
|
sinout = temp + x; // 1 add
|
|
cosout = temp - x; // 1 sub
|
|
} else if (quarter == 1) {
|
|
// Second quarter, angle = pi/2 .. pi
|
|
float x = 0.5f - modphase; // 1 sub
|
|
float temp = (2 - 4*C)*x*x + C; // 2 mul, 1 add
|
|
sinout = x + temp; // 1 add
|
|
cosout = x - temp; // 1 sub
|
|
} else if (quarter == 2) {
|
|
// Third quarter, angle = pi .. 1.5pi
|
|
float x = modphase - 0.5f; // 1 sub
|
|
float temp = (4*C - 2)*x*x - C; // 2 mul, 1 sub
|
|
sinout = temp - x; // 1 sub
|
|
cosout = temp + x; // 1 add
|
|
} else if (quarter == 3) {
|
|
// Fourth quarter, angle = 1.5pi..2pi
|
|
float x = modphase - 0.5f; // 1 sub
|
|
float temp = (2 - 4*C)*x*x + C; // 2 mul, 1 add
|
|
sinout = x - temp; // 1 sub
|
|
cosout = x + temp; // 1 add
|
|
}
|
|
}
|
|
#undef C
|
|
|
|
|
|
const float PI_SQR = 9.86960440108935861883449099987615114f;
|
|
|
|
//https://code.google.com/p/math-neon/source/browse/trunk/math_floorf.c?r=18
|
|
// About 2 correct decimals. Not great.
|
|
void fcs2(float theta, float &outsine, float &outcosine) {
|
|
float gamma = theta + 1;
|
|
gamma += 2;
|
|
gamma /= 4;
|
|
theta += 2;
|
|
theta /= 4;
|
|
//theta -= (float)(int)theta;
|
|
//gamma -= (float)(int)gamma;
|
|
theta -= floorf(theta);
|
|
gamma -= floorf(gamma);
|
|
theta *= 4;
|
|
theta -= 2;
|
|
gamma *= 4;
|
|
gamma -= 2;
|
|
|
|
float x = 2 * gamma - gamma * fabs(gamma);
|
|
float y = 2 * theta - theta * fabs(theta);
|
|
const float P = 0.225f;
|
|
outsine = P * (y * fabsf(y) - y) + y; // Q * y + P * y * abs(y)
|
|
outcosine = P * (x * fabsf(x) - x) + x; // Q * y + P * y * abs(y)
|
|
}
|
|
|
|
|
|
|
|
void fastsincos(float x, float &sine, float &cosine) {
|
|
fcs2(x, sine, cosine);
|
|
}
|
|
|
|
bool TestSinCos() {
|
|
for (int i = -100; i <= 100; i++) {
|
|
float f = i / 30.0f;
|
|
|
|
// The PSP sin/cos take as argument angle * M_PI_2.
|
|
// We need to match that.
|
|
float slowsin = sinf(f * M_PI_2), slowcos = cosf(f * M_PI_2);
|
|
float fastsin, fastcos;
|
|
fastsincos(f, fastsin, fastcos);
|
|
if (g_testLog) {
|
|
printf("%f: slow: %0.8f, %0.8f fast: %0.8f, %0.8f\n", f, slowsin, slowcos, fastsin, fastcos);
|
|
}
|
|
}
|
|
return true;
|
|
}
|
|
|
|
|
|
bool TestAsin() {
|
|
for (int i = -100; i <= 100; i++) {
|
|
float f = i / 100.0f;
|
|
float slowval = asinf(f) / M_PI_2;
|
|
float fastval = fastasin5(f) / M_PI_2;
|
|
if (g_testLog) {
|
|
printf("slow: %0.16f fast: %0.16f\n", slowval, fastval);
|
|
}
|
|
float diff = fabsf(slowval - fastval);
|
|
// EXPECT_TRUE(diff < 0.0001f);
|
|
}
|
|
// EXPECT_TRUE(fastasin(1.0) / M_PI_2 <= 1.0f);
|
|
return true;
|
|
}
|
|
|
|
bool TestMathUtil() {
|
|
EXPECT_FALSE(my_isinf(1.0));
|
|
volatile float zero = 0.0f;
|
|
EXPECT_TRUE(my_isinf(1.0f/zero));
|
|
EXPECT_FALSE(my_isnan(1.0f/zero));
|
|
return true;
|
|
}
|
|
|
|
bool TestParsers() {
|
|
const char *macstr = "01:02:03:ff:fe:fd";
|
|
uint8_t mac[6];
|
|
ParseMacAddress(macstr, mac);
|
|
EXPECT_TRUE(mac[0] == 1);
|
|
EXPECT_TRUE(mac[1] == 2);
|
|
EXPECT_TRUE(mac[2] == 3);
|
|
EXPECT_TRUE(mac[3] == 255);
|
|
EXPECT_TRUE(mac[4] == 254);
|
|
EXPECT_TRUE(mac[5] == 253);
|
|
return true;
|
|
}
|
|
|
|
bool TestTruncateCpy() {
|
|
// Normal in-bounds copy.
|
|
char buf[8];
|
|
size_t len = truncate_cpy_len(buf, "abc", 3);
|
|
EXPECT_EQ_INT((int)len, 3);
|
|
EXPECT_TRUE(strcmp(buf, "abc") == 0);
|
|
|
|
// Exact fit (source length is Count - 1).
|
|
len = truncate_cpy_len(buf, "abcdefg", 7);
|
|
EXPECT_EQ_INT((int)len, 7);
|
|
EXPECT_TRUE(strcmp(buf, "abcdefg") == 0);
|
|
|
|
// Overflow - truncated to Count - 1 chars.
|
|
len = truncate_cpy_len(buf, "abcdefghij", 10);
|
|
EXPECT_EQ_INT((int)len, 7);
|
|
EXPECT_TRUE(strcmp(buf, "abcdefg") == 0);
|
|
|
|
// Zero-length source used to underflow to out[-1].
|
|
buf[0] = 'X';
|
|
len = truncate_cpy_len(buf, "", 0);
|
|
EXPECT_EQ_INT((int)len, 0);
|
|
EXPECT_EQ_INT((int)buf[0], 0);
|
|
|
|
// Simple concatenation.
|
|
char catBuf[16];
|
|
len = truncate_cat(catBuf, sizeof(catBuf), "abc", 3, "def", 3);
|
|
EXPECT_EQ_INT((int)len, 6);
|
|
EXPECT_TRUE(strcmp(catBuf, "abcdef") == 0);
|
|
|
|
// Truncation when the combined length exceeds the buffer.
|
|
len = truncate_cat(catBuf, 8, "abcd", 4, "efghij", 6);
|
|
EXPECT_EQ_INT((int)len, 7);
|
|
EXPECT_TRUE(strcmp(catBuf, "abcdefg") == 0);
|
|
|
|
// src1 alone already fills/overflows the buffer.
|
|
len = truncate_cat(catBuf, 4, "abcdefg", 7, "xyz", 3);
|
|
EXPECT_EQ_INT((int)len, 3);
|
|
EXPECT_TRUE(strcmp(catBuf, "abc") == 0);
|
|
|
|
// Both empty used to underflow to out[-1].
|
|
catBuf[0] = 'X';
|
|
len = truncate_cat(catBuf, sizeof(catBuf), "", 0, "", 0);
|
|
EXPECT_EQ_INT((int)len, 0);
|
|
EXPECT_EQ_INT((int)catBuf[0], 0);
|
|
return true;
|
|
}
|
|
|
|
bool TestUtf8() {
|
|
// Valid multi-byte UTF-8 (ASCII + 2-byte 'é' + 3-byte '€') round-trips unchanged.
|
|
const std::string valid = "abc \xC3\xA9 \xE2\x82\xAC";
|
|
EXPECT_TRUE(SanitizeUTF8(valid) == valid);
|
|
|
|
// u8_nextchar must stop at the end of the buffer instead of reading past a
|
|
// truncated multi-byte sequence (a lead byte with no continuation bytes).
|
|
{
|
|
std::string s = "abc";
|
|
s += (char)0xF4;
|
|
int index = 3;
|
|
int size = (int)s.size();
|
|
uint32_t c = u8_nextchar(s.data(), &index, size);
|
|
EXPECT_EQ_INT(index, size);
|
|
EXPECT_EQ_INT((int)c, 0xF4);
|
|
}
|
|
|
|
// A long run of stray continuation bytes must not walk off the end of the
|
|
// internal offsetsFromUTF8 table (used to read arbitrarily far out of bounds).
|
|
{
|
|
std::string s(32, (char)0x80);
|
|
int index = 0;
|
|
int size = (int)s.size();
|
|
uint32_t c = u8_nextchar(s.data(), &index, size);
|
|
EXPECT_TRUE(index > 0 && index <= size);
|
|
}
|
|
|
|
// SanitizeUTF8 on a string that ends mid-sequence must not read or write past
|
|
// the buffer, and must preserve the well-formed leading portion.
|
|
{
|
|
std::string truncated = "abc";
|
|
truncated += (char)0xF4;
|
|
std::string sanitized = SanitizeUTF8(truncated);
|
|
EXPECT_TRUE(sanitized.substr(0, 3) == "abc");
|
|
}
|
|
|
|
// ConvertUTF8ToJavaModifiedUTF8 must simply drop an incomplete trailing
|
|
// sequence rather than asserting or crashing.
|
|
{
|
|
std::string input = "abc";
|
|
input += (char)0xF0;
|
|
std::string output;
|
|
ConvertUTF8ToJavaModifiedUTF8(&output, input);
|
|
EXPECT_TRUE(output == "abc");
|
|
}
|
|
|
|
// ReplaceInvalidUTF8 must always return well-formed UTF-8, keeping the good parts. This one
|
|
// guards a WebSocket text frame (memory.readString reads arbitrary emulated memory), where a
|
|
// single bad byte getting through disconnects conforming clients.
|
|
{
|
|
const std::string replacement = "\xEF\xBF\xBD"; // U+FFFD
|
|
|
|
// Valid input is returned untouched, including 1/2/3/4-byte sequences.
|
|
const std::string allValid = "abc \xC3\xA9 \xE2\x82\xAC \xF0\x9F\x8E\xAE";
|
|
EXPECT_TRUE(ReplaceInvalidUTF8(allValid) == allValid);
|
|
EXPECT_TRUE(ReplaceInvalidUTF8("") == "");
|
|
|
|
// Unlike SanitizeUTF8, it keeps going past the bad byte instead of truncating there.
|
|
EXPECT_TRUE(ReplaceInvalidUTF8(std::string("ab\xFF" "cd")) == "ab" + replacement + "cd");
|
|
|
|
// One replacement per bad byte, and resynchronization on the next valid sequence.
|
|
EXPECT_TRUE(ReplaceInvalidUTF8(std::string("\x80\x80")) == replacement + replacement);
|
|
EXPECT_TRUE(ReplaceInvalidUTF8(std::string("\xC3")) == replacement);
|
|
EXPECT_TRUE(ReplaceInvalidUTF8(std::string("\xC3?")) == replacement + "?");
|
|
|
|
// Sequences that lenient decoders accept but that aren't legal UTF-8: overlong encodings,
|
|
// surrogates, and anything past U+10FFFF.
|
|
EXPECT_TRUE(ReplaceInvalidUTF8(std::string("\xC0\xAF")) == replacement + replacement);
|
|
EXPECT_TRUE(ReplaceInvalidUTF8(std::string("\xE0\x80\xAF")) == replacement + replacement + replacement);
|
|
EXPECT_TRUE(ReplaceInvalidUTF8(std::string("\xED\xA0\x80")) == replacement + replacement + replacement);
|
|
EXPECT_TRUE(ReplaceInvalidUTF8(std::string("\xF4\x90\x80\x80")) == replacement + replacement + replacement + replacement);
|
|
|
|
// Whatever the input, the output must itself survive a re-run unchanged - i.e. be valid.
|
|
for (int b = 0; b < 256; ++b) {
|
|
std::string input = "a";
|
|
input += (char)b;
|
|
input += "b";
|
|
const std::string once = ReplaceInvalidUTF8(input);
|
|
EXPECT_TRUE(ReplaceInvalidUTF8(once) == once);
|
|
}
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
// PointerWrap is the savestate serializer. The same DoState() code runs in MEASURE, WRITE and READ
|
|
// mode, so mistakes here don't show up as compile errors - they show up as savestates that don't
|
|
// load, or worse. Everything read back came off disk and is therefore attacker-controllable, so the
|
|
// corrupt-input cases below matter as much as the round trips.
|
|
|
|
struct SerializerPOD {
|
|
u32 a;
|
|
s16 b;
|
|
u8 c;
|
|
float d;
|
|
};
|
|
|
|
// Held by pointer in a map below, the way a lot of HLE state is (sceMpeg's contexts, sceFont's
|
|
// fonts, sceKernelThread's pending calls, ...).
|
|
struct SerializerTestObj {
|
|
u32 value = 0;
|
|
void DoState(PointerWrap &p) {
|
|
Do(p, value);
|
|
}
|
|
};
|
|
|
|
// Shaped like real DoState() code: a versioned section, a few fields, and one field that only
|
|
// exists from version 2 on. Set version to 1 before serializing to produce an old-format buffer.
|
|
struct SerializerTestState {
|
|
int version = 2;
|
|
u32 a = 0;
|
|
std::string name;
|
|
std::vector<u32> values;
|
|
int addedInV2 = 0;
|
|
|
|
void DoState(PointerWrap &p) {
|
|
PointerWrapSection s = p.Section("TestState", 1, version);
|
|
if (!s)
|
|
return;
|
|
Do(p, a);
|
|
Do(p, name);
|
|
Do(p, values);
|
|
if (s >= 2)
|
|
Do(p, addedInV2);
|
|
}
|
|
};
|
|
|
|
// Measures, then rewinds into a buffer of exactly the measured size - the same sequence
|
|
// CChunkFileReader::MeasureAndSavePtr() uses, so the measure-vs-write checkpoint machinery gets
|
|
// exercised as well. Returns false if either pass reported an error or the two disagreed.
|
|
template <class Func>
|
|
static bool SerializerWrite(std::vector<u8> *out, Func f) {
|
|
u8 *ptr = nullptr;
|
|
PointerWrap p(&ptr, PointerWrap::MODE_MEASURE);
|
|
f(p);
|
|
if (p.Failed())
|
|
return false;
|
|
// Fill with junk so a field the write pass forgets shows up as garbage rather than zero.
|
|
out->assign(p.Offset(), 0xCD);
|
|
p.RewindForWrite(out->empty() ? nullptr : &(*out)[0]);
|
|
f(p);
|
|
return p.CheckAfterWrite() && !p.Failed();
|
|
}
|
|
|
|
// Reads out of a copy of the buffer with the read end set, which is what LoadPtr() does and what
|
|
// all the bounds checks depend on.
|
|
template <class Func>
|
|
static PointerWrap::Error SerializerRead(const std::vector<u8> &buf, Func f) {
|
|
std::vector<u8> copy = buf;
|
|
u8 *ptr = copy.empty() ? nullptr : ©[0];
|
|
PointerWrap p(&ptr, PointerWrap::MODE_READ);
|
|
if (!copy.empty())
|
|
p.SetReadEnd(©[0] + copy.size());
|
|
f(p);
|
|
return p.error;
|
|
}
|
|
|
|
// A buffer whose first four bytes are a length/count field, for feeding hand-corrupted values in.
|
|
static std::vector<u8> SerializerBufferWithCount(int count, size_t totalSize) {
|
|
std::vector<u8> buf(totalSize < sizeof(int) ? sizeof(int) : totalSize, 0);
|
|
memcpy(&buf[0], &count, sizeof(int));
|
|
return buf;
|
|
}
|
|
|
|
bool TestSerializer() {
|
|
// Plain values and PODs survive a measure/write/read round trip, and the measure pass agrees
|
|
// with the write pass about the size.
|
|
{
|
|
SerializerPOD pod{ 0x12345678, -1234, 0xAB, 1.5f };
|
|
u32 plain = 0xDEADBEEF;
|
|
std::vector<u8> buf;
|
|
EXPECT_TRUE(SerializerWrite(&buf, [&](PointerWrap &p) {
|
|
Do(p, plain);
|
|
Do(p, pod);
|
|
}));
|
|
EXPECT_EQ_INT((int)buf.size(), (int)(sizeof(u32) + sizeof(SerializerPOD)));
|
|
|
|
u32 outPlain = 0;
|
|
SerializerPOD outPod{};
|
|
EXPECT_EQ_INT((int)SerializerRead(buf, [&](PointerWrap &p) {
|
|
Do(p, outPlain);
|
|
Do(p, outPod);
|
|
}), (int)PointerWrap::ERROR_NONE);
|
|
EXPECT_EQ_HEX(outPlain, plain);
|
|
EXPECT_EQ_HEX(outPod.a, pod.a);
|
|
EXPECT_EQ_INT(outPod.b, pod.b);
|
|
EXPECT_EQ_INT(outPod.c, pod.c);
|
|
EXPECT_EQ_FLOAT(outPod.d, pod.d);
|
|
}
|
|
|
|
// Strings, including the empty one and one with an embedded NUL - the length is serialized
|
|
// separately, so the NUL shouldn't truncate anything.
|
|
{
|
|
std::string empty;
|
|
std::string normal = "hello savestate";
|
|
std::string embedded("a\0b", 3);
|
|
std::vector<u8> buf;
|
|
EXPECT_TRUE(SerializerWrite(&buf, [&](PointerWrap &p) {
|
|
Do(p, empty);
|
|
Do(p, normal);
|
|
Do(p, embedded);
|
|
}));
|
|
|
|
std::string outEmpty = "junk", outNormal, outEmbedded;
|
|
EXPECT_EQ_INT((int)SerializerRead(buf, [&](PointerWrap &p) {
|
|
Do(p, outEmpty);
|
|
Do(p, outNormal);
|
|
Do(p, outEmbedded);
|
|
}), (int)PointerWrap::ERROR_NONE);
|
|
EXPECT_TRUE(outEmpty.empty());
|
|
EXPECT_EQ_STR(outNormal, normal);
|
|
EXPECT_EQ_INT((int)outEmbedded.size(), 3);
|
|
EXPECT_TRUE(outEmbedded == embedded);
|
|
}
|
|
|
|
// The containers that DoState() code actually uses.
|
|
{
|
|
std::vector<u32> vec{ 1, 2, 3, 0xFFFFFFFF };
|
|
std::vector<std::string> strs{ "one", "", "three" };
|
|
std::map<u32, u32> map{ { 5, 50 }, { 1, 10 }, { 9, 90 } };
|
|
std::set<u32> set{ 7, 3, 11 };
|
|
std::list<u32> list{ 4, 5, 6 };
|
|
std::vector<u8> buf;
|
|
EXPECT_TRUE(SerializerWrite(&buf, [&](PointerWrap &p) {
|
|
Do(p, vec);
|
|
Do(p, strs);
|
|
Do(p, map);
|
|
Do(p, set);
|
|
Do(p, list);
|
|
}));
|
|
|
|
// Deliberately non-empty to start with, so a load that forgets to clear shows up.
|
|
std::vector<u32> outVec{ 99, 99 };
|
|
std::vector<std::string> outStrs{ "junk" };
|
|
std::map<u32, u32> outMap{ { 123, 456 } };
|
|
std::set<u32> outSet{ 123 };
|
|
std::list<u32> outList{ 99 };
|
|
EXPECT_EQ_INT((int)SerializerRead(buf, [&](PointerWrap &p) {
|
|
Do(p, outVec);
|
|
Do(p, outStrs);
|
|
Do(p, outMap);
|
|
Do(p, outSet);
|
|
Do(p, outList);
|
|
}), (int)PointerWrap::ERROR_NONE);
|
|
EXPECT_TRUE(outVec == vec);
|
|
EXPECT_TRUE(outStrs == strs);
|
|
EXPECT_TRUE(outMap == map);
|
|
EXPECT_TRUE(outSet == set);
|
|
EXPECT_TRUE(outList == list);
|
|
}
|
|
|
|
// Sections: a matching title and an acceptable version give a usable section, and the marker
|
|
// the section destructor writes lines up on read.
|
|
{
|
|
SerializerTestState state;
|
|
state.a = 0x1234;
|
|
state.name = "statename";
|
|
state.values = { 10, 20 };
|
|
state.addedInV2 = 77;
|
|
std::vector<u8> buf;
|
|
EXPECT_TRUE(SerializerWrite(&buf, [&](PointerWrap &p) { state.DoState(p); }));
|
|
|
|
SerializerTestState out;
|
|
EXPECT_EQ_INT((int)SerializerRead(buf, [&](PointerWrap &p) { out.DoState(p); }), (int)PointerWrap::ERROR_NONE);
|
|
EXPECT_EQ_HEX(out.a, state.a);
|
|
EXPECT_EQ_STR(out.name, state.name);
|
|
EXPECT_TRUE(out.values == state.values);
|
|
EXPECT_EQ_INT(out.addedInV2, state.addedInV2);
|
|
|
|
// A section written by a newer build than we understand must be refused, not
|
|
// misinterpreted - this is what stops a future savestate from being read as garbage.
|
|
bool sectionUsable = true;
|
|
EXPECT_EQ_INT((int)SerializerRead(buf, [&](PointerWrap &p) {
|
|
PointerWrapSection s = p.Section("TestState", 1, 1);
|
|
sectionUsable = (bool)s;
|
|
}), (int)PointerWrap::ERROR_FAILURE);
|
|
EXPECT_FALSE(sectionUsable);
|
|
|
|
// So must a different section title where we expected this one.
|
|
sectionUsable = true;
|
|
EXPECT_EQ_INT((int)SerializerRead(buf, [&](PointerWrap &p) {
|
|
PointerWrapSection s = p.Section("SomethingElse", 1, 2);
|
|
sectionUsable = (bool)s;
|
|
}), (int)PointerWrap::ERROR_FAILURE);
|
|
EXPECT_FALSE(sectionUsable);
|
|
}
|
|
|
|
// The backwards compatibility mechanism itself: a version 1 buffer read by version 2 code
|
|
// yields a version 1 section, and the field that didn't exist yet keeps its default.
|
|
{
|
|
SerializerTestState old;
|
|
old.version = 1;
|
|
old.a = 0xAAAA;
|
|
old.name = "old";
|
|
old.values = { 1 };
|
|
old.addedInV2 = 12345; // Not written at version 1.
|
|
std::vector<u8> buf;
|
|
EXPECT_TRUE(SerializerWrite(&buf, [&](PointerWrap &p) { old.DoState(p); }));
|
|
|
|
SerializerTestState out; // version 2, addedInV2 defaults to 0
|
|
EXPECT_EQ_INT((int)SerializerRead(buf, [&](PointerWrap &p) { out.DoState(p); }), (int)PointerWrap::ERROR_NONE);
|
|
EXPECT_EQ_HEX(out.a, old.a);
|
|
EXPECT_EQ_STR(out.name, old.name);
|
|
EXPECT_EQ_INT(out.addedInV2, 0);
|
|
}
|
|
|
|
// A truncated savestate has to fail cleanly at every possible cut point rather than read past
|
|
// the end of the buffer. This is the case a corrupt file on disk actually produces.
|
|
{
|
|
SerializerTestState state;
|
|
state.a = 0x5555;
|
|
state.name = "truncate me";
|
|
state.values = { 1, 2, 3, 4, 5 };
|
|
std::vector<u8> buf;
|
|
EXPECT_TRUE(SerializerWrite(&buf, [&](PointerWrap &p) { state.DoState(p); }));
|
|
|
|
for (size_t cut = 1; cut < buf.size(); ++cut) {
|
|
std::vector<u8> truncated(buf.begin(), buf.begin() + cut);
|
|
SerializerTestState out;
|
|
const PointerWrap::Error err = SerializerRead(truncated, [&](PointerWrap &p) { out.DoState(p); });
|
|
if (err != PointerWrap::ERROR_FAILURE) {
|
|
printf("Truncating to %d of %d bytes was accepted\n", (int)cut, (int)buf.size());
|
|
return false;
|
|
}
|
|
}
|
|
}
|
|
|
|
// Hand-corrupted counts and lengths. In each case there is nowhere near enough buffer left for
|
|
// what the header claims, so the load must be refused before anything is allocated or copied.
|
|
{
|
|
// A vector claiming four billion elements.
|
|
{
|
|
std::vector<u8> buf = SerializerBufferWithCount((int)0xFFFFFFFF, 64);
|
|
std::vector<u32> out;
|
|
EXPECT_EQ_INT((int)SerializerRead(buf, [&](PointerWrap &p) { Do(p, out); }), (int)PointerWrap::ERROR_FAILURE);
|
|
EXPECT_TRUE(out.empty());
|
|
}
|
|
// A map, a set and a list claiming the same.
|
|
{
|
|
std::vector<u8> buf = SerializerBufferWithCount((int)0xFFFFFFFF, 64);
|
|
std::map<u32, u32> outMap;
|
|
std::set<u32> outSet;
|
|
std::list<u32> outList;
|
|
EXPECT_EQ_INT((int)SerializerRead(buf, [&](PointerWrap &p) { Do(p, outMap); }), (int)PointerWrap::ERROR_FAILURE);
|
|
EXPECT_EQ_INT((int)SerializerRead(buf, [&](PointerWrap &p) { Do(p, outSet); }), (int)PointerWrap::ERROR_FAILURE);
|
|
EXPECT_EQ_INT((int)SerializerRead(buf, [&](PointerWrap &p) { Do(p, outList); }), (int)PointerWrap::ERROR_FAILURE);
|
|
EXPECT_TRUE(outMap.empty());
|
|
EXPECT_TRUE(outSet.empty());
|
|
EXPECT_TRUE(outList.empty());
|
|
}
|
|
// Strings: negative, absurd, zero (there is always at least a NUL byte), and merely longer
|
|
// than what's left in the buffer.
|
|
{
|
|
const int lengths[] = { -1, 0x7FFFFFFF, 0, 1000 };
|
|
for (size_t i = 0; i < ARRAY_SIZE(lengths); ++i) {
|
|
std::vector<u8> buf = SerializerBufferWithCount(lengths[i], 64);
|
|
std::string out = "untouched";
|
|
const PointerWrap::Error err = SerializerRead(buf, [&](PointerWrap &p) { Do(p, out); });
|
|
if (err != PointerWrap::ERROR_FAILURE) {
|
|
printf("String length %d was accepted\n", lengths[i]);
|
|
return false;
|
|
}
|
|
}
|
|
}
|
|
// u16strings are measured in bytes, so on top of the above they can also claim a length
|
|
// that isn't a whole number of characters.
|
|
{
|
|
const int lengths[] = { -1, 0x7FFFFFFF, 0, 1, 3, 1000 };
|
|
for (size_t i = 0; i < ARRAY_SIZE(lengths); ++i) {
|
|
std::vector<u8> buf = SerializerBufferWithCount(lengths[i], 64);
|
|
std::u16string out = u"untouched";
|
|
const PointerWrap::Error err = SerializerRead(buf, [&](PointerWrap &p) { Do(p, out); });
|
|
if (err != PointerWrap::ERROR_FAILURE) {
|
|
printf("u16string byte length %d was accepted\n", lengths[i]);
|
|
return false;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// Maps of pointers, which is how most HLE contexts are savestated. Loading deletes whatever
|
|
// was in the map before reading the new contents, so bailing out on a corrupt count must not
|
|
// leave the freed pointers behind - the next access to them, or the destructor, would be a
|
|
// use-after-free.
|
|
{
|
|
std::map<u32, SerializerTestObj *> ptrMap;
|
|
ptrMap[1] = new SerializerTestObj();
|
|
ptrMap[1]->value = 0x1111;
|
|
ptrMap[7] = new SerializerTestObj();
|
|
ptrMap[7]->value = 0x7777;
|
|
std::vector<u8> buf;
|
|
EXPECT_TRUE(SerializerWrite(&buf, [&](PointerWrap &p) { Do(p, ptrMap); }));
|
|
|
|
std::map<u32, SerializerTestObj *> outMap;
|
|
outMap[99] = new SerializerTestObj();
|
|
EXPECT_EQ_INT((int)SerializerRead(buf, [&](PointerWrap &p) { Do(p, outMap); }), (int)PointerWrap::ERROR_NONE);
|
|
EXPECT_EQ_INT((int)outMap.size(), 2);
|
|
EXPECT_EQ_HEX(outMap[1]->value, (u32)0x1111);
|
|
EXPECT_EQ_HEX(outMap[7]->value, (u32)0x7777);
|
|
|
|
std::vector<u8> badBuf = SerializerBufferWithCount((int)0xFFFFFFFF, 64);
|
|
EXPECT_EQ_INT((int)SerializerRead(badBuf, [&](PointerWrap &p) { Do(p, outMap); }), (int)PointerWrap::ERROR_FAILURE);
|
|
const bool leftDangling = !outMap.empty();
|
|
outMap.clear(); // Must not delete these - the loader already did.
|
|
EXPECT_FALSE(leftDangling);
|
|
|
|
for (const std::pair<const u32, SerializerTestObj *> &entry : ptrMap)
|
|
delete entry.second;
|
|
}
|
|
|
|
// Valid u16strings still round trip, including the empty one.
|
|
{
|
|
std::u16string empty;
|
|
std::u16string text = u"unicode";
|
|
std::vector<u8> buf;
|
|
EXPECT_TRUE(SerializerWrite(&buf, [&](PointerWrap &p) {
|
|
Do(p, empty);
|
|
Do(p, text);
|
|
}));
|
|
std::u16string outEmpty = u"junk", outText;
|
|
EXPECT_EQ_INT((int)SerializerRead(buf, [&](PointerWrap &p) {
|
|
Do(p, outEmpty);
|
|
Do(p, outText);
|
|
}), (int)PointerWrap::ERROR_NONE);
|
|
EXPECT_TRUE(outEmpty.empty());
|
|
EXPECT_TRUE(outText == text);
|
|
}
|
|
|
|
// Once a failure is latched the serializer drops to MODE_NOOP and stops touching the caller's
|
|
// data, so the rest of a broken savestate can be walked without doing damage. A warning, on the
|
|
// other hand, must not stop anything.
|
|
{
|
|
std::vector<u8> buf = SerializerBufferWithCount(-1, 64);
|
|
u32 shouldBeUntouched = 0x11111111;
|
|
std::string alsoUntouched = "keepme";
|
|
bool wentNoop = false;
|
|
const PointerWrap::Error err = SerializerRead(buf, [&](PointerWrap &p) {
|
|
std::string bad;
|
|
Do(p, bad); // fails: negative length
|
|
wentNoop = p.mode == PointerWrap::MODE_NOOP;
|
|
Do(p, shouldBeUntouched);
|
|
Do(p, alsoUntouched);
|
|
});
|
|
EXPECT_EQ_INT((int)err, (int)PointerWrap::ERROR_FAILURE);
|
|
EXPECT_TRUE(wentNoop);
|
|
EXPECT_EQ_HEX(shouldBeUntouched, (u32)0x11111111);
|
|
EXPECT_EQ_STR(alsoUntouched, std::string("keepme"));
|
|
|
|
u8 *ptr = &buf[0];
|
|
PointerWrap p(&ptr, PointerWrap::MODE_READ);
|
|
p.SetError(PointerWrap::ERROR_WARNING);
|
|
EXPECT_FALSE(p.Failed());
|
|
EXPECT_EQ_INT((int)p.mode, (int)PointerWrap::MODE_READ);
|
|
}
|
|
|
|
// A marker that doesn't match means the writer and reader disagree about the layout, which has
|
|
// to be a hard failure - carrying on would read every following field from the wrong offset.
|
|
{
|
|
std::vector<u8> buf;
|
|
EXPECT_TRUE(SerializerWrite(&buf, [&](PointerWrap &p) { p.DoMarker("Thing", 0x1234); }));
|
|
EXPECT_EQ_INT((int)SerializerRead(buf, [&](PointerWrap &p) { p.DoMarker("Thing", 0x1234); }), (int)PointerWrap::ERROR_NONE);
|
|
EXPECT_EQ_INT((int)SerializerRead(buf, [&](PointerWrap &p) { p.DoMarker("Thing", 0x4321); }), (int)PointerWrap::ERROR_FAILURE);
|
|
}
|
|
|
|
// The measure pass and the write pass have to visit the same sections at the same offsets;
|
|
// CheckAfterWrite() exists to catch DoState() code whose behaviour depends on something that
|
|
// changed in between. Fake exactly that and make sure it's noticed rather than silently
|
|
// producing a savestate that can't be loaded.
|
|
{
|
|
int pass = 0;
|
|
std::vector<u8> buf;
|
|
EXPECT_FALSE(SerializerWrite(&buf, [&](PointerWrap &p) {
|
|
u32 v = 0;
|
|
PointerWrapSection s = p.Section(pass++ == 0 ? "SectionA" : "SectionB", 1);
|
|
if (s)
|
|
Do(p, v);
|
|
}));
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
bool TestMemBlockInfoSaveState() {
|
|
MemBlockInfoInit();
|
|
MemBlockOverrideDetailed();
|
|
|
|
// Split the single initial slab (which spans the whole address space) into several
|
|
// pieces, so the savestate has more than just the first slab.
|
|
NotifyMemInfo(MemBlockFlags::ALLOC, 0x08800000, 0x1000, "InitialTag", 10);
|
|
NotifyMemInfo(MemBlockFlags::ALLOC, 0x08810000, 0x1000, "SecondTag", 9);
|
|
// FindMemInfo flushes pending notifications into the actual slab maps.
|
|
FindMemInfo(0x08800000, 0x20000);
|
|
|
|
// Round-trip through the savestate serializer. This used to leave every slab but
|
|
// the first with an uninitialized tagLen, which MemSlabMap::Split() would later use
|
|
// as an unbounded memcpy length into a fixed 128 byte buffer, corrupting the heap.
|
|
uint8_t *measurePtr = nullptr;
|
|
PointerWrap pm(&measurePtr, PointerWrap::MODE_MEASURE);
|
|
MemBlockInfoDoState(pm);
|
|
size_t stateSize = (size_t)measurePtr;
|
|
EXPECT_TRUE(stateSize > 0);
|
|
|
|
std::vector<uint8_t> buffer(stateSize);
|
|
uint8_t *writePtr = &buffer[0];
|
|
PointerWrap pw(&writePtr, PointerWrap::MODE_WRITE);
|
|
MemBlockInfoDoState(pw);
|
|
|
|
uint8_t *readPtr = &buffer[0];
|
|
PointerWrap pr(&readPtr, PointerWrap::MODE_READ);
|
|
MemBlockInfoDoState(pr);
|
|
|
|
// Force a split on a slab that was just loaded from the savestate - this is what used
|
|
// to corrupt the heap (or crash outright) before the fix.
|
|
NotifyMemInfo(MemBlockFlags::ALLOC, 0x08800100, 0x10, "SplitTag", 8);
|
|
auto results = FindMemInfo(0x08800000, 0x20000);
|
|
EXPECT_TRUE(!results.empty());
|
|
|
|
MemBlockReleaseDetailed();
|
|
MemBlockInfoShutdown();
|
|
return true;
|
|
}
|
|
|
|
// Covers BreakpointManager::ChangeBreakPointAddress(), which the ImDebugger uses to relocate a
|
|
// breakpoint the user is editing. Only the pure bookkeeping is exercised here - there's no JIT in
|
|
// this build, so the cache invalidation it also does is a no-op.
|
|
bool TestBreakpoints() {
|
|
const u32 kAddrA = 0x08804000;
|
|
const u32 kAddrB = 0x08804100;
|
|
const u32 kAddrC = 0x08804200;
|
|
|
|
g_breakpoints.AddBreakPoint(kAddrA);
|
|
g_breakpoints.ChangeBreakPoint(kAddrA, BreakAction(BREAK_ACTION_PAUSE | BREAK_ACTION_LOG));
|
|
// Pretend it tripped a few times, so the reset below is actually testing something.
|
|
g_breakpoints.GetBreakpointRefs()[0].numHits = 7;
|
|
|
|
// A plain move: gone from the old address, present at the new one, action carried over, and the
|
|
// hit count (which belonged to the old address) reset.
|
|
EXPECT_TRUE(g_breakpoints.ChangeBreakPointAddress(kAddrA, kAddrB));
|
|
EXPECT_FALSE(g_breakpoints.IsAddressBreakPoint(kAddrA));
|
|
EXPECT_TRUE(g_breakpoints.IsAddressBreakPoint(kAddrB));
|
|
{
|
|
std::vector<BreakPoint> bps = g_breakpoints.GetBreakpoints();
|
|
EXPECT_EQ_INT((int)bps.size(), 1);
|
|
EXPECT_EQ_INT((int)bps[0].action, (int)(BREAK_ACTION_PAUSE | BREAK_ACTION_LOG));
|
|
EXPECT_EQ_INT((int)bps[0].numHits, 0);
|
|
}
|
|
|
|
// Moving onto an address that already has a breakpoint must be refused rather than creating a
|
|
// duplicate - FindBreakpoint() only ever returns one entry per address, so the other would be
|
|
// silently dead. Neither breakpoint should move.
|
|
g_breakpoints.AddBreakPoint(kAddrC);
|
|
EXPECT_FALSE(g_breakpoints.ChangeBreakPointAddress(kAddrB, kAddrC));
|
|
EXPECT_TRUE(g_breakpoints.IsAddressBreakPoint(kAddrB));
|
|
EXPECT_TRUE(g_breakpoints.IsAddressBreakPoint(kAddrC));
|
|
EXPECT_EQ_INT((int)g_breakpoints.GetBreakpoints().size(), 2);
|
|
|
|
// Nothing to move.
|
|
EXPECT_FALSE(g_breakpoints.ChangeBreakPointAddress(kAddrA, 0x08804300));
|
|
EXPECT_FALSE(g_breakpoints.IsAddressBreakPoint(0x08804300));
|
|
|
|
// Moving somewhere it already is succeeds and does nothing.
|
|
EXPECT_TRUE(g_breakpoints.ChangeBreakPointAddress(kAddrB, kAddrB));
|
|
EXPECT_TRUE(g_breakpoints.IsAddressBreakPoint(kAddrB));
|
|
EXPECT_EQ_INT((int)g_breakpoints.GetBreakpoints().size(), 2);
|
|
|
|
g_breakpoints.RemoveBreakPoint(kAddrB);
|
|
g_breakpoints.RemoveBreakPoint(kAddrC);
|
|
EXPECT_EQ_INT((int)g_breakpoints.GetBreakpoints().size(), 0);
|
|
return true;
|
|
}
|
|
|
|
// The one-shot breakpoint behind step-over/step-out/run-until. It deliberately lives outside the
|
|
// user's breakpoint list, so the two must not be able to see or clobber each other.
|
|
bool TestTempBreakpoints() {
|
|
const u32 kAddrA = 0x08804000;
|
|
const u32 kAddrB = 0x08804100;
|
|
|
|
// ExecBreakPoint's log path asks the symbol map to describe the address, and the unit test
|
|
// build leaves g_symbolMap null (the emulator always creates one at boot).
|
|
SymbolMap symbolMap;
|
|
g_symbolMap = &symbolMap;
|
|
|
|
g_breakpoints.SetTempBreakPoint(kAddrA);
|
|
EXPECT_TRUE(g_breakpoints.HasTempBreakPoint());
|
|
// Invisible to the user's list, but the interpreter and JIT still have to check the address.
|
|
EXPECT_EQ_INT((int)g_breakpoints.GetBreakpoints().size(), 0);
|
|
EXPECT_FALSE(g_breakpoints.IsAddressBreakPoint(kAddrA));
|
|
EXPECT_TRUE(g_breakpoints.NeedsBreakCheckAt(kAddrA));
|
|
EXPECT_TRUE(g_breakpoints.RangeContainsBreakPoint(kAddrA - 4, 16));
|
|
// This one is the trap: with no user breakpoints at all, the run loops and the JIT skip
|
|
// breakpoint checking entirely unless HasBreakPoints() accounts for the temporary one.
|
|
EXPECT_TRUE(g_breakpoints.HasBreakPoints());
|
|
|
|
// Only one at a time - a second request replaces rather than accumulating.
|
|
g_breakpoints.SetTempBreakPoint(kAddrB);
|
|
EXPECT_FALSE(g_breakpoints.NeedsBreakCheckAt(kAddrA));
|
|
EXPECT_TRUE(g_breakpoints.NeedsBreakCheckAt(kAddrB));
|
|
|
|
// A log-only user breakpoint at the same address is the case that used to break stepping: the
|
|
// user breakpoint must log without stopping, and the pending step must still complete.
|
|
g_breakpoints.AddBreakPoint(kAddrB);
|
|
g_breakpoints.ChangeBreakPoint(kAddrB, BREAK_ACTION_LOG);
|
|
EXPECT_TRUE(g_breakpoints.HasTempBreakPoint());
|
|
{
|
|
std::vector<BreakPoint> bps = g_breakpoints.GetBreakpoints();
|
|
EXPECT_EQ_INT((int)bps.size(), 1);
|
|
EXPECT_EQ_INT((int)bps[0].action, (int)BREAK_ACTION_LOG);
|
|
}
|
|
{
|
|
// Both fire: the log from the user breakpoint, the pause from the temporary one.
|
|
BreakAction action = g_breakpoints.ExecBreakPoint(kAddrB);
|
|
EXPECT_TRUE((action & BREAK_ACTION_LOG) != 0);
|
|
EXPECT_TRUE((action & BREAK_ACTION_PAUSE) != 0);
|
|
EXPECT_EQ_INT((int)g_breakpoints.GetBreakpoints()[0].numHits, 1);
|
|
}
|
|
|
|
// Removing the user breakpoint must not take the temporary one with it, and vice versa.
|
|
EXPECT_TRUE(g_breakpoints.HasTempBreakPoint());
|
|
g_breakpoints.RemoveBreakPoint(kAddrB);
|
|
EXPECT_EQ_INT((int)g_breakpoints.GetBreakpoints().size(), 0);
|
|
EXPECT_TRUE(g_breakpoints.HasTempBreakPoint());
|
|
EXPECT_TRUE(g_breakpoints.NeedsBreakCheckAt(kAddrB));
|
|
|
|
g_breakpoints.ClearTempBreakPoint();
|
|
EXPECT_FALSE(g_breakpoints.HasTempBreakPoint());
|
|
EXPECT_FALSE(g_breakpoints.NeedsBreakCheckAt(kAddrB));
|
|
EXPECT_FALSE(g_breakpoints.HasBreakPoints());
|
|
|
|
// A user breakpoint alone still behaves normally after all that.
|
|
g_breakpoints.AddBreakPoint(kAddrA);
|
|
EXPECT_TRUE(g_breakpoints.IsAddressBreakPoint(kAddrA));
|
|
EXPECT_TRUE((g_breakpoints.ExecBreakPoint(kAddrA) & BREAK_ACTION_PAUSE) != 0);
|
|
g_breakpoints.RemoveBreakPoint(kAddrA);
|
|
EXPECT_FALSE(g_breakpoints.HasBreakPoints());
|
|
|
|
g_symbolMap = nullptr;
|
|
return true;
|
|
}
|
|
|
|
// BlockAllocator backs sceKernelAllocPartitionMemory and friends. It's pure address bookkeeping -
|
|
// no real memory involved - which makes it cheap to check hard: after any sequence of operations
|
|
// the blocks must still tile the range exactly, and the free-space accessors must match reality.
|
|
|
|
// Rebuilds the block list through the public accessors and checks it tiles [start, start+size)
|
|
// with no gaps, overlaps or strays, then cross-checks GetTotalFreeBytes/GetLargestFreeBlockSize
|
|
// against what is actually in the list.
|
|
static bool ValidateAllocator(BlockAllocator &a, u32 rangeStart, u32 rangeSize) {
|
|
u32 addr = rangeStart;
|
|
u32 totalFree = 0;
|
|
u32 largestFree = 0;
|
|
int guard = 0;
|
|
while (addr < rangeStart + rangeSize) {
|
|
const u32 blockStart = a.GetBlockStartFromAddress(addr);
|
|
const u32 blockSize = a.GetBlockSizeFromAddress(addr);
|
|
if (blockStart != addr)
|
|
return false; // a gap, or the block misreports where it starts
|
|
if (blockSize == 0 || blockSize == (u32)-1)
|
|
return false;
|
|
if ((u64)blockStart + blockSize > (u64)rangeStart + rangeSize)
|
|
return false; // runs off the end of the range
|
|
if (a.IsBlockFree(addr)) {
|
|
totalFree += blockSize;
|
|
if (blockSize > largestFree)
|
|
largestFree = blockSize;
|
|
}
|
|
addr += blockSize;
|
|
if (++guard > 200000)
|
|
return false; // cycle in the list
|
|
}
|
|
if (addr != rangeStart + rangeSize)
|
|
return false; // last block overshot the end
|
|
if (a.GetTotalFreeBytes() != totalFree)
|
|
return false;
|
|
if (a.GetLargestFreeBlockSize() != largestFree)
|
|
return false;
|
|
return true;
|
|
}
|
|
|
|
bool TestBlockAllocator() {
|
|
const u32 kStart = 0x08800000;
|
|
const u32 kSize = 0x00100000; // 1MB
|
|
const u32 kGrain = 256;
|
|
|
|
// A fresh allocator is one free block covering everything.
|
|
{
|
|
BlockAllocator a(kGrain);
|
|
a.Init(kStart, kSize, false);
|
|
EXPECT_TRUE(ValidateAllocator(a, kStart, kSize));
|
|
EXPECT_EQ_INT((int)a.GetTotalFreeBytes(), (int)kSize);
|
|
EXPECT_EQ_INT((int)a.GetLargestFreeBlockSize(), (int)kSize);
|
|
EXPECT_TRUE(a.IsBlockFree(kStart));
|
|
}
|
|
|
|
// Bottom-up allocation starts at the bottom; top-down ends at the top.
|
|
{
|
|
BlockAllocator a(kGrain);
|
|
a.Init(kStart, kSize, false);
|
|
|
|
u32 sizeA = 0x1000;
|
|
const u32 addrA = a.Alloc(sizeA, false, "bottom");
|
|
EXPECT_EQ_INT((int)addrA, (int)kStart);
|
|
EXPECT_FALSE(a.IsBlockFree(addrA));
|
|
|
|
u32 sizeB = 0x1000;
|
|
const u32 addrB = a.Alloc(sizeB, true, "top");
|
|
EXPECT_EQ_INT((int)(addrB + sizeB), (int)(kStart + kSize));
|
|
EXPECT_TRUE(ValidateAllocator(a, kStart, kSize));
|
|
EXPECT_EQ_INT((int)a.GetTotalFreeBytes(), (int)(kSize - sizeA - sizeB));
|
|
|
|
EXPECT_TRUE(a.Free(addrA));
|
|
EXPECT_TRUE(a.Free(addrB));
|
|
EXPECT_EQ_INT((int)a.GetLargestFreeBlockSize(), (int)kSize);
|
|
EXPECT_TRUE(ValidateAllocator(a, kStart, kSize));
|
|
}
|
|
|
|
// Sizes are rounded up to the grain, and the caller is told about it.
|
|
{
|
|
BlockAllocator a(kGrain);
|
|
a.Init(kStart, kSize, false);
|
|
u32 size = 1;
|
|
const u32 addr = a.Alloc(size, false, "tiny");
|
|
EXPECT_FALSE(addr == (u32)-1);
|
|
EXPECT_EQ_INT((int)size, (int)kGrain);
|
|
EXPECT_EQ_INT((int)a.GetBlockSizeFromAddress(addr), (int)kGrain);
|
|
EXPECT_TRUE(ValidateAllocator(a, kStart, kSize));
|
|
}
|
|
|
|
// Nonsense sizes are refused rather than wrapping into something huge.
|
|
{
|
|
BlockAllocator a(kGrain);
|
|
a.Init(kStart, kSize, false);
|
|
u32 zero = 0;
|
|
EXPECT_EQ_INT((int)a.Alloc(zero, false, "zero"), -1);
|
|
u32 huge = kSize + 1;
|
|
EXPECT_EQ_INT((int)a.Alloc(huge, false, "huge"), -1);
|
|
// A failed allocation must not have disturbed anything.
|
|
EXPECT_EQ_INT((int)a.GetTotalFreeBytes(), (int)kSize);
|
|
EXPECT_TRUE(ValidateAllocator(a, kStart, kSize));
|
|
}
|
|
|
|
// Freeing the middle of three leaves a hole; freeing its neighbours merges it all back.
|
|
{
|
|
BlockAllocator a(kGrain);
|
|
a.Init(kStart, kSize, false);
|
|
u32 s1 = 0x10000, s2 = 0x10000, s3 = 0x10000;
|
|
const u32 a1 = a.Alloc(s1, false, "1");
|
|
const u32 a2 = a.Alloc(s2, false, "2");
|
|
const u32 a3 = a.Alloc(s3, false, "3");
|
|
EXPECT_TRUE(a1 < a2 && a2 < a3);
|
|
|
|
EXPECT_TRUE(a.Free(a2));
|
|
EXPECT_TRUE(a.IsBlockFree(a2));
|
|
EXPECT_EQ_INT((int)a.GetBlockSizeFromAddress(a2), (int)s2);
|
|
EXPECT_TRUE(ValidateAllocator(a, kStart, kSize));
|
|
|
|
EXPECT_TRUE(a.Free(a1));
|
|
// a1 and a2 are adjacent and both free now, so they must have become one block.
|
|
EXPECT_EQ_INT((int)a.GetBlockStartFromAddress(a2), (int)a1);
|
|
EXPECT_EQ_INT((int)a.GetBlockSizeFromAddress(a1), (int)(s1 + s2));
|
|
EXPECT_TRUE(ValidateAllocator(a, kStart, kSize));
|
|
|
|
EXPECT_TRUE(a.Free(a3));
|
|
EXPECT_EQ_INT((int)a.GetLargestFreeBlockSize(), (int)kSize);
|
|
EXPECT_TRUE(ValidateAllocator(a, kStart, kSize));
|
|
}
|
|
|
|
// Double free, and freeing an address that was never allocated, must fail rather than corrupt.
|
|
{
|
|
BlockAllocator a(kGrain);
|
|
a.Init(kStart, kSize, false);
|
|
u32 size = 0x1000;
|
|
const u32 addr = a.Alloc(size, false, "once");
|
|
EXPECT_TRUE(a.Free(addr));
|
|
EXPECT_FALSE(a.Free(addr));
|
|
EXPECT_FALSE(a.Free(kStart + kSize + 0x1000)); // outside the range entirely
|
|
EXPECT_TRUE(ValidateAllocator(a, kStart, kSize));
|
|
}
|
|
|
|
// AllocAt places a block exactly, and refuses if it is already taken.
|
|
{
|
|
BlockAllocator a(kGrain);
|
|
a.Init(kStart, kSize, false);
|
|
const u32 target = kStart + 0x20000;
|
|
const u32 got = a.AllocAt(target, 0x1000, "at");
|
|
EXPECT_EQ_INT((int)got, (int)target);
|
|
EXPECT_FALSE(a.IsBlockFree(target));
|
|
EXPECT_TRUE(a.IsBlockFree(kStart)); // the space below it stays free
|
|
EXPECT_TRUE(ValidateAllocator(a, kStart, kSize));
|
|
|
|
EXPECT_EQ_INT((int)a.AllocAt(target, 0x1000, "again"), -1);
|
|
EXPECT_EQ_INT((int)a.AllocAt(target + 0x800, 0x100, "overlap"), -1);
|
|
EXPECT_TRUE(ValidateAllocator(a, kStart, kSize));
|
|
|
|
EXPECT_TRUE(a.Free(target));
|
|
EXPECT_EQ_INT((int)a.GetLargestFreeBlockSize(), (int)kSize);
|
|
}
|
|
|
|
// AllocAligned honours a coarser alignment than the allocator's own grain.
|
|
{
|
|
BlockAllocator a(16);
|
|
a.Init(kStart + 16, kSize, false); // deliberately not 4K-aligned to start with
|
|
u32 skew = 0x30;
|
|
EXPECT_FALSE(a.Alloc(skew, false, "skew") == (u32)-1);
|
|
|
|
u32 size = 0x1000;
|
|
const u32 addr = a.AllocAligned(size, 0x1000, 0x1000, false, "aligned");
|
|
EXPECT_FALSE(addr == (u32)-1);
|
|
EXPECT_EQ_INT((int)(addr & 0xFFF), 0);
|
|
EXPECT_TRUE(ValidateAllocator(a, kStart + 16, kSize));
|
|
|
|
u32 topSize = 0x1000;
|
|
const u32 topAddr = a.AllocAligned(topSize, 0x1000, 0x1000, true, "aligned-top");
|
|
EXPECT_FALSE(topAddr == (u32)-1);
|
|
EXPECT_EQ_INT((int)(topAddr & 0xFFF), 0);
|
|
EXPECT_TRUE(ValidateAllocator(a, kStart + 16, kSize));
|
|
}
|
|
|
|
// Fill the range completely, then drain it - nothing should leak or go missing.
|
|
{
|
|
BlockAllocator a(kGrain);
|
|
a.Init(kStart, kSize, false);
|
|
std::vector<u32> addrs;
|
|
for (;;) {
|
|
u32 size = 0x4000;
|
|
const u32 addr = a.Alloc(size, false, "fill");
|
|
if (addr == (u32)-1)
|
|
break;
|
|
addrs.push_back(addr);
|
|
}
|
|
EXPECT_EQ_INT((int)addrs.size(), (int)(kSize / 0x4000));
|
|
EXPECT_EQ_INT((int)a.GetTotalFreeBytes(), 0);
|
|
EXPECT_TRUE(ValidateAllocator(a, kStart, kSize));
|
|
|
|
for (u32 addr : addrs)
|
|
EXPECT_TRUE(a.Free(addr));
|
|
EXPECT_EQ_INT((int)a.GetTotalFreeBytes(), (int)kSize);
|
|
EXPECT_EQ_INT((int)a.GetLargestFreeBlockSize(), (int)kSize);
|
|
EXPECT_TRUE(ValidateAllocator(a, kStart, kSize));
|
|
}
|
|
|
|
// AllocAt with a position that is not on the grain: it must still round down to a whole block
|
|
// and keep the range tiled, and it reports back how much the caller actually got from their
|
|
// requested position (which is less than a whole block, since the block starts lower).
|
|
{
|
|
BlockAllocator a(kGrain);
|
|
a.Init(kStart, kSize, false);
|
|
const u32 unaligned = kStart + 0x2010;
|
|
u32 size = 0x100;
|
|
const u32 got = a.AllocAt(unaligned, size, "unaligned");
|
|
EXPECT_EQ_INT((int)got, (int)unaligned);
|
|
EXPECT_TRUE(ValidateAllocator(a, kStart, kSize));
|
|
// The block it landed in starts at the grain boundary below.
|
|
EXPECT_EQ_INT((int)a.GetBlockStartFromAddress(unaligned), (int)(kStart + 0x2000));
|
|
EXPECT_FALSE(a.IsBlockFree(unaligned));
|
|
// Free() takes any address inside the block, so the address AllocAt handed back works.
|
|
EXPECT_TRUE(a.Free(got));
|
|
EXPECT_EQ_INT((int)a.GetLargestFreeBlockSize(), (int)kSize);
|
|
EXPECT_TRUE(ValidateAllocator(a, kStart, kSize));
|
|
}
|
|
|
|
// FreeExact only accepts the true start of a block, so an unaligned AllocAt address is
|
|
// rejected - worth pinning down, since Free() and FreeExact() differ here.
|
|
{
|
|
BlockAllocator a(kGrain);
|
|
a.Init(kStart, kSize, false);
|
|
const u32 unaligned = kStart + 0x2010;
|
|
u32 size = 0x100;
|
|
EXPECT_EQ_INT((int)a.AllocAt(unaligned, size, "unaligned"), (int)unaligned);
|
|
EXPECT_FALSE(a.FreeExact(unaligned));
|
|
EXPECT_TRUE(a.FreeExact(kStart + 0x2000));
|
|
EXPECT_TRUE(ValidateAllocator(a, kStart, kSize));
|
|
}
|
|
|
|
// A range whose size is not a multiple of the grain. The leftover tail can never be handed
|
|
// out, but it must not break the tiling or the accounting.
|
|
{
|
|
BlockAllocator a(kGrain);
|
|
const u32 oddSize = 0x10000 + 0x10;
|
|
a.Init(kStart, oddSize, false);
|
|
EXPECT_TRUE(ValidateAllocator(a, kStart, oddSize));
|
|
std::vector<u32> addrs;
|
|
for (;;) {
|
|
u32 size = 0x1000;
|
|
const u32 addr = a.Alloc(size, false, "odd");
|
|
if (addr == (u32)-1)
|
|
break;
|
|
addrs.push_back(addr);
|
|
EXPECT_TRUE(ValidateAllocator(a, kStart, oddSize));
|
|
}
|
|
for (size_t j = 0; j < addrs.size(); ++j)
|
|
EXPECT_TRUE(a.Free(addrs[j]));
|
|
EXPECT_EQ_INT((int)a.GetTotalFreeBytes(), (int)oddSize);
|
|
EXPECT_TRUE(ValidateAllocator(a, kStart, oddSize));
|
|
}
|
|
|
|
// Churn again, this time mixing in aligned allocations and AllocAt so the block list gets into
|
|
// shapes the plain alloc/free loop never produces.
|
|
{
|
|
BlockAllocator a(16);
|
|
a.Init(kStart, kSize, false);
|
|
std::vector<u32> live;
|
|
u32 rng = 987654321;
|
|
auto next = [&rng]() { rng = rng * 1103515245u + 12345u; return (rng >> 16) & 0x7FFF; };
|
|
|
|
for (int i = 0; i < 4000; ++i) {
|
|
const int op = next() % 100;
|
|
if (op < 30 && !live.empty()) {
|
|
const size_t idx = next() % live.size();
|
|
EXPECT_TRUE(a.Free(live[idx]));
|
|
live.erase(live.begin() + idx);
|
|
} else if (op < 60) {
|
|
u32 size = ((next() % 32) + 1) * 16;
|
|
const u32 addr = a.Alloc(size, (next() % 2) != 0, "churn2");
|
|
if (addr != (u32)-1)
|
|
live.push_back(addr);
|
|
} else if (op < 90) {
|
|
// Alignments of 16, 64, 256, 1024, 4096.
|
|
const u32 align = 16u << ((next() % 5) * 2);
|
|
u32 size = ((next() % 32) + 1) * 16;
|
|
const u32 addr = a.AllocAligned(size, align, align, (next() % 2) != 0, "aligned2");
|
|
if (addr != (u32)-1) {
|
|
if ((addr & (align - 1)) != 0) {
|
|
printf("AllocAligned returned %08x for alignment %08x at iteration %d\n", addr, align, i);
|
|
return false;
|
|
}
|
|
live.push_back(addr);
|
|
}
|
|
} else {
|
|
const u32 pos = kStart + ((next() % (kSize / 0x1000)) * 0x1000);
|
|
const u32 addr = a.AllocAt(pos, 0x800, "at2");
|
|
if (addr != (u32)-1)
|
|
live.push_back(addr);
|
|
}
|
|
if (!ValidateAllocator(a, kStart, kSize)) {
|
|
printf("BlockAllocator invariant broken at iteration %d (op %d)\n", i, op);
|
|
return false;
|
|
}
|
|
}
|
|
|
|
for (size_t j = 0; j < live.size(); ++j)
|
|
EXPECT_TRUE(a.Free(live[j]));
|
|
EXPECT_EQ_INT((int)a.GetTotalFreeBytes(), (int)kSize);
|
|
EXPECT_EQ_INT((int)a.GetLargestFreeBlockSize(), (int)kSize);
|
|
EXPECT_TRUE(ValidateAllocator(a, kStart, kSize));
|
|
}
|
|
|
|
// Randomised churn. Fixed seed so a failure is reproducible; the point is to reach block
|
|
// layouts hand-written cases would not, while checking the invariants after every step.
|
|
{
|
|
BlockAllocator a(kGrain);
|
|
a.Init(kStart, kSize, false);
|
|
std::vector<std::pair<u32, u32> > live; // address, size
|
|
u32 rng = 12345;
|
|
auto next = [&rng]() { rng = rng * 1103515245u + 12345u; return (rng >> 16) & 0x7FFF; };
|
|
|
|
for (int i = 0; i < 3000; ++i) {
|
|
const bool doAlloc = live.empty() || (next() % 100) < 55;
|
|
if (doAlloc) {
|
|
u32 size = ((next() % 64) + 1) * kGrain;
|
|
const bool fromTop = (next() % 2) != 0;
|
|
const u32 addr = a.Alloc(size, fromTop, "churn");
|
|
if (addr != (u32)-1) {
|
|
// It must not overlap anything already handed out.
|
|
for (size_t j = 0; j < live.size(); ++j) {
|
|
const bool overlaps = addr < live[j].first + live[j].second && live[j].first < addr + size;
|
|
EXPECT_FALSE(overlaps);
|
|
}
|
|
live.push_back(std::make_pair(addr, size));
|
|
}
|
|
} else {
|
|
const size_t idx = next() % live.size();
|
|
EXPECT_TRUE(a.Free(live[idx].first));
|
|
live.erase(live.begin() + idx);
|
|
}
|
|
if (!ValidateAllocator(a, kStart, kSize)) {
|
|
printf("BlockAllocator invariant broken at iteration %d\n", i);
|
|
return false;
|
|
}
|
|
}
|
|
|
|
for (size_t j = 0; j < live.size(); ++j)
|
|
EXPECT_TRUE(a.Free(live[j].first));
|
|
// Everything given back means one free block again - if merging ever misses a case, this
|
|
// is where it shows up.
|
|
EXPECT_EQ_INT((int)a.GetTotalFreeBytes(), (int)kSize);
|
|
EXPECT_EQ_INT((int)a.GetLargestFreeBlockSize(), (int)kSize);
|
|
EXPECT_TRUE(ValidateAllocator(a, kStart, kSize));
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
// SymbolMap holds the function/data/label tables the debugger and disassembler read. Symbols are
|
|
// stored relative to a module so they survive that module being unloaded and reloaded elsewhere,
|
|
// and only symbols belonging to a currently-loaded module count as "active". That indirection is
|
|
// where the surprises live, so most of this is about module lifetime and the shared label table.
|
|
bool TestSymbolMap() {
|
|
const u32 kModStart = 0x08804000;
|
|
const u32 kModSize = 0x00010000;
|
|
|
|
// Functions are found by containing address, not just by their start.
|
|
{
|
|
SymbolMap map;
|
|
map.AddModule("TEST", kModStart, kModSize);
|
|
map.AddFunction("func_a", kModStart + 0x100, 0x40);
|
|
map.AddFunction("func_b", kModStart + 0x200, 0x80);
|
|
map.SortSymbols();
|
|
|
|
EXPECT_EQ_INT((int)map.GetFunctionStart(kModStart + 0x100), (int)(kModStart + 0x100));
|
|
EXPECT_EQ_INT((int)map.GetFunctionStart(kModStart + 0x120), (int)(kModStart + 0x100));
|
|
EXPECT_EQ_INT((int)map.GetFunctionStart(kModStart + 0x13C), (int)(kModStart + 0x100));
|
|
// One past the end belongs to nobody.
|
|
EXPECT_EQ_INT((int)map.GetFunctionStart(kModStart + 0x140), (int)SymbolMap::INVALID_ADDRESS);
|
|
EXPECT_EQ_INT((int)map.GetFunctionSize(kModStart + 0x100), 0x40);
|
|
EXPECT_EQ_INT((int)map.GetFunctionStart(kModStart + 0x27F), (int)(kModStart + 0x200));
|
|
|
|
// AddFunction doubles as a label, so the name is reachable both ways.
|
|
EXPECT_EQ_STR(map.GetLabelString(kModStart + 0x100), std::string("func_a"));
|
|
u32 value = 0;
|
|
EXPECT_TRUE(map.GetLabelValue("func_b", value));
|
|
EXPECT_EQ_INT((int)value, (int)(kModStart + 0x200));
|
|
}
|
|
|
|
// SetFunctionSize and RemoveFunction.
|
|
{
|
|
SymbolMap map;
|
|
map.AddModule("TEST", kModStart, kModSize);
|
|
map.AddFunction("func", kModStart + 0x100, 0x40);
|
|
map.SortSymbols();
|
|
|
|
EXPECT_TRUE(map.SetFunctionSize(kModStart + 0x100, 0x80));
|
|
EXPECT_EQ_INT((int)map.GetFunctionSize(kModStart + 0x100), 0x80);
|
|
EXPECT_EQ_INT((int)map.GetFunctionStart(kModStart + 0x170), (int)(kModStart + 0x100));
|
|
|
|
EXPECT_TRUE(map.RemoveFunction(kModStart + 0x100, true));
|
|
map.SortSymbols();
|
|
EXPECT_EQ_INT((int)map.GetFunctionStart(kModStart + 0x100), (int)SymbolMap::INVALID_ADDRESS);
|
|
// Removing something that isn't there fails rather than doing damage.
|
|
EXPECT_FALSE(map.RemoveFunction(kModStart + 0x100, true));
|
|
}
|
|
|
|
// Data symbols work the same way, and carry a type.
|
|
{
|
|
SymbolMap map;
|
|
map.AddModule("TEST", kModStart, kModSize);
|
|
map.AddData(kModStart + 0x400, 0x20, DATATYPE_WORD);
|
|
map.SortSymbols();
|
|
|
|
EXPECT_EQ_INT((int)map.GetDataStart(kModStart + 0x400), (int)(kModStart + 0x400));
|
|
EXPECT_EQ_INT((int)map.GetDataStart(kModStart + 0x41F), (int)(kModStart + 0x400));
|
|
EXPECT_EQ_INT((int)map.GetDataStart(kModStart + 0x420), (int)SymbolMap::INVALID_ADDRESS);
|
|
EXPECT_EQ_INT((int)map.GetDataSize(kModStart + 0x400), 0x20);
|
|
EXPECT_EQ_INT((int)map.GetDataType(kModStart + 0x400), (int)DATATYPE_WORD);
|
|
}
|
|
|
|
// Symbols only count as active while their module is loaded, and they come back - at the new
|
|
// address - when it is loaded somewhere else. This is the whole point of storing them
|
|
// module-relative.
|
|
{
|
|
SymbolMap map;
|
|
map.AddModule("TEST", kModStart, kModSize);
|
|
map.AddFunction("func", kModStart + 0x100, 0x40);
|
|
map.SortSymbols();
|
|
EXPECT_EQ_INT((int)map.GetAllActiveSymbols(ST_FUNCTION).size(), 1);
|
|
|
|
map.UnloadModule(kModStart, kModSize);
|
|
EXPECT_EQ_INT((int)map.GetAllActiveSymbols(ST_FUNCTION).size(), 0);
|
|
EXPECT_EQ_INT((int)map.GetFunctionStart(kModStart + 0x100), (int)SymbolMap::INVALID_ADDRESS);
|
|
|
|
// Same module, different load address - the symbol should follow it.
|
|
const u32 newStart = kModStart + 0x100000;
|
|
map.AddModule("TEST", newStart, kModSize);
|
|
map.SortSymbols();
|
|
EXPECT_EQ_INT((int)map.GetAllActiveSymbols(ST_FUNCTION).size(), 1);
|
|
EXPECT_EQ_INT((int)map.GetFunctionStart(newStart + 0x100), (int)(newStart + 0x100));
|
|
EXPECT_EQ_INT((int)map.GetFunctionStart(kModStart + 0x100), (int)SymbolMap::INVALID_ADDRESS);
|
|
}
|
|
|
|
// Symbols outside any module are stored against module index 0 ("absolute"), which is always
|
|
// considered loaded - that's what makes labelling a heap or stack address work.
|
|
{
|
|
SymbolMap map;
|
|
map.AddModule("TEST", kModStart, kModSize);
|
|
const u32 outside = 0x0BFBF800; // stack, well outside the module
|
|
EXPECT_EQ_INT(map.GetModuleIndex(outside), -1);
|
|
map.AddData(outside, 0x10, DATATYPE_BYTE, 0);
|
|
map.AddLabel("stackthing", outside, 0);
|
|
map.SortSymbols();
|
|
|
|
EXPECT_EQ_INT((int)map.GetDataStart(outside), (int)outside);
|
|
EXPECT_EQ_STR(map.GetLabelString(outside), std::string("stackthing"));
|
|
// Unloading the module must not take an unrelated absolute symbol with it.
|
|
map.UnloadModule(kModStart, kModSize);
|
|
EXPECT_EQ_INT((int)map.GetDataStart(outside), (int)outside);
|
|
}
|
|
|
|
// Labels are one table shared by functions and data, and AddLabel deliberately leaves an
|
|
// existing one alone. Pinning this down because it surprises people: hle.data.add reports the
|
|
// name you asked for while the map keeps the old one, unless the caller forces it.
|
|
{
|
|
SymbolMap map;
|
|
map.AddModule("TEST", kModStart, kModSize);
|
|
const u32 addr = kModStart + 0x100;
|
|
map.AddFunction("original", addr, 0x40);
|
|
map.SortSymbols();
|
|
EXPECT_EQ_STR(map.GetLabelString(addr), std::string("original"));
|
|
|
|
map.AddLabel("replacement", addr);
|
|
EXPECT_EQ_STR(map.GetLabelString(addr), std::string("original"));
|
|
|
|
// SetLabelName is the way to actually change it...
|
|
map.SetLabelName("replacement", addr);
|
|
EXPECT_EQ_STR(map.GetLabelString(addr), std::string("replacement"));
|
|
// ...and because the table is shared, that renamed the function too.
|
|
std::vector<SymbolEntry> funcs = map.GetAllActiveSymbols(ST_FUNCTION);
|
|
EXPECT_EQ_INT((int)funcs.size(), 1);
|
|
EXPECT_EQ_STR(funcs[0].name, std::string("replacement"));
|
|
}
|
|
|
|
// Likewise, removing a data symbol with removeName drops the shared label, which is why
|
|
// hle.data.remove has to check whether a function is using it first.
|
|
{
|
|
SymbolMap map;
|
|
map.AddModule("TEST", kModStart, kModSize);
|
|
const u32 addr = kModStart + 0x100;
|
|
map.AddFunction("shared", addr, 0x40);
|
|
map.AddData(addr, 0x10, DATATYPE_BYTE);
|
|
map.SortSymbols();
|
|
EXPECT_EQ_STR(map.GetLabelString(addr), std::string("shared"));
|
|
|
|
EXPECT_TRUE(map.RemoveData(addr, true));
|
|
map.SortSymbols();
|
|
// The function is still there, but its name is gone with the label.
|
|
EXPECT_EQ_INT((int)map.GetFunctionStart(addr), (int)addr);
|
|
EXPECT_TRUE(map.GetLabelString(addr).empty());
|
|
|
|
// Whereas removeName=false leaves the label for the function that still needs it.
|
|
SymbolMap map2;
|
|
map2.AddModule("TEST", kModStart, kModSize);
|
|
map2.AddFunction("kept", addr, 0x40);
|
|
map2.AddData(addr, 0x10, DATATYPE_BYTE);
|
|
map2.SortSymbols();
|
|
EXPECT_TRUE(map2.RemoveData(addr, false));
|
|
map2.SortSymbols();
|
|
EXPECT_EQ_STR(map2.GetLabelString(addr), std::string("kept"));
|
|
}
|
|
|
|
// GetSymbolInfo / GetDescription round out what the disassembler asks for.
|
|
{
|
|
SymbolMap map;
|
|
map.AddModule("TEST", kModStart, kModSize);
|
|
map.AddFunction("described", kModStart + 0x100, 0x40);
|
|
map.SortSymbols();
|
|
|
|
SymbolInfo info{};
|
|
EXPECT_TRUE(map.GetSymbolInfo(&info, kModStart + 0x110, ST_FUNCTION));
|
|
EXPECT_EQ_INT((int)info.address, (int)(kModStart + 0x100));
|
|
EXPECT_EQ_INT((int)info.size, 0x40);
|
|
EXPECT_FALSE(map.GetSymbolInfo(&info, kModStart + 0x900, ST_FUNCTION));
|
|
EXPECT_EQ_STR(map.GetDescription(kModStart + 0x100), std::string("described"));
|
|
}
|
|
|
|
// Clear really clears, including the module table.
|
|
{
|
|
SymbolMap map;
|
|
map.AddModule("TEST", kModStart, kModSize);
|
|
map.AddFunction("func", kModStart + 0x100, 0x40);
|
|
map.AddData(kModStart + 0x400, 0x20, DATATYPE_WORD);
|
|
map.SortSymbols();
|
|
EXPECT_EQ_INT((int)map.GetAllActiveSymbols(ST_FUNCTION).size(), 1);
|
|
|
|
map.Clear();
|
|
EXPECT_EQ_INT((int)map.GetAllActiveSymbols(ST_FUNCTION).size(), 0);
|
|
EXPECT_EQ_INT((int)map.GetAllActiveSymbols(ST_DATA).size(), 0);
|
|
EXPECT_EQ_INT((int)map.getAllModules().size(), 0);
|
|
EXPECT_EQ_INT((int)map.GetFunctionStart(kModStart + 0x100), (int)SymbolMap::INVALID_ADDRESS);
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
// DenseHashMap/PrehashMap are open-addressed, linear-probing maps used in hot GPU paths - the
|
|
// texture cache, the shader managers, the software renderer's sampler/drawpixel caches. They use
|
|
// tombstones for removal, which is where the interesting failure modes live.
|
|
static void *HashValue(int i) {
|
|
return (void *)(uintptr_t)(i + 1); // never null, so GetOrNull can tell "missing" apart
|
|
}
|
|
|
|
bool TestHashmaps() {
|
|
// The basics: insert, find, miss, remove, size.
|
|
{
|
|
DenseHashMap<uint32_t, void *> m(16);
|
|
EXPECT_EQ_INT((int)m.size(), 0);
|
|
EXPECT_TRUE(m.Insert(100, HashValue(1)));
|
|
EXPECT_TRUE(m.Insert(200, HashValue(2)));
|
|
EXPECT_EQ_INT((int)m.size(), 2);
|
|
|
|
void *v = nullptr;
|
|
EXPECT_TRUE(m.Get(100, &v));
|
|
EXPECT_TRUE(v == HashValue(1));
|
|
EXPECT_TRUE(m.Get(200, &v));
|
|
EXPECT_TRUE(v == HashValue(2));
|
|
EXPECT_FALSE(m.Get(300, &v));
|
|
EXPECT_TRUE(m.ContainsKey(100));
|
|
EXPECT_FALSE(m.ContainsKey(300));
|
|
EXPECT_TRUE(m.GetOrNull(300) == nullptr);
|
|
|
|
EXPECT_TRUE(m.Remove(100));
|
|
EXPECT_EQ_INT((int)m.size(), 1);
|
|
EXPECT_FALSE(m.Get(100, &v));
|
|
// Removing what isn't there says so rather than corrupting the map.
|
|
EXPECT_FALSE(m.Remove(100));
|
|
EXPECT_FALSE(m.Remove(999));
|
|
// The other entry must still be reachable - a tombstone can't cut the probe chain.
|
|
EXPECT_TRUE(m.Get(200, &v));
|
|
}
|
|
|
|
// Iterate visits exactly the live entries, and Clear empties it.
|
|
{
|
|
DenseHashMap<uint32_t, void *> m(16);
|
|
for (int i = 0; i < 6; i++)
|
|
EXPECT_TRUE(m.Insert(i, HashValue(i)));
|
|
EXPECT_TRUE(m.Remove(2));
|
|
EXPECT_TRUE(m.Remove(4));
|
|
|
|
int seen = 0;
|
|
uint32_t keyMask = 0;
|
|
bool valuesOk = true;
|
|
m.Iterate([&](const uint32_t &key, void *value) {
|
|
seen++;
|
|
keyMask |= 1u << key;
|
|
// The value must still be the one that went in with this key.
|
|
if (value != HashValue((int)key))
|
|
valuesOk = false;
|
|
});
|
|
EXPECT_TRUE(valuesOk);
|
|
EXPECT_EQ_INT(seen, 4);
|
|
EXPECT_EQ_INT((int)keyMask, (int)((1u << 0) | (1u << 1) | (1u << 3) | (1u << 5)));
|
|
|
|
m.Clear();
|
|
EXPECT_EQ_INT((int)m.size(), 0);
|
|
void *v = nullptr;
|
|
EXPECT_FALSE(m.Get(0, &v));
|
|
// Still usable after Clear.
|
|
EXPECT_TRUE(m.Insert(0, HashValue(42)));
|
|
EXPECT_TRUE(m.Get(0, &v));
|
|
}
|
|
|
|
// Growing past the initial capacity must not lose or corrupt anything.
|
|
{
|
|
DenseHashMap<uint32_t, void *> m(8);
|
|
const int kCount = 500;
|
|
for (int i = 0; i < kCount; i++)
|
|
EXPECT_TRUE(m.Insert(i * 7 + 1, HashValue(i)));
|
|
EXPECT_EQ_INT((int)m.size(), kCount);
|
|
for (int i = 0; i < kCount; i++) {
|
|
void *v = nullptr;
|
|
EXPECT_TRUE(m.Get(i * 7 + 1, &v));
|
|
EXPECT_TRUE(v == HashValue(i));
|
|
}
|
|
// And nothing that was never inserted has appeared.
|
|
for (int i = 0; i < kCount; i++) {
|
|
void *v = nullptr;
|
|
EXPECT_FALSE(m.Get(i * 7 + 2, &v));
|
|
}
|
|
}
|
|
|
|
// Rebuild() compacts away tombstones without changing what's in the map.
|
|
{
|
|
DenseHashMap<uint32_t, void *> m(64);
|
|
for (int i = 0; i < 20; i++)
|
|
EXPECT_TRUE(m.Insert(i, HashValue(i)));
|
|
for (int i = 0; i < 20; i += 2)
|
|
EXPECT_TRUE(m.Remove(i));
|
|
m.Rebuild();
|
|
EXPECT_EQ_INT((int)m.size(), 10);
|
|
for (int i = 1; i < 20; i += 2) {
|
|
void *v = nullptr;
|
|
EXPECT_TRUE(m.Get(i, &v));
|
|
EXPECT_TRUE(v == HashValue(i));
|
|
}
|
|
for (int i = 0; i < 20; i += 2) {
|
|
void *v = nullptr;
|
|
EXPECT_FALSE(m.Get(i, &v));
|
|
}
|
|
}
|
|
|
|
// Differential test against std::unordered_map. Fixed seed so a failure reproduces.
|
|
{
|
|
DenseHashMap<uint32_t, void *> m(16);
|
|
std::unordered_map<uint32_t, void *> ref;
|
|
uint32_t rng = 24680;
|
|
auto next = [&rng]() { rng = rng * 1103515245u + 12345u; return (rng >> 16) & 0x7FFF; };
|
|
|
|
for (int i = 0; i < 20000; i++) {
|
|
const uint32_t key = next() % 500;
|
|
if ((next() % 100) < 55) {
|
|
if (ref.find(key) == ref.end()) {
|
|
void *value = HashValue((int)key);
|
|
EXPECT_TRUE(m.Insert(key, value));
|
|
ref[key] = value;
|
|
}
|
|
} else {
|
|
const bool had = ref.find(key) != ref.end();
|
|
EXPECT_EQ_INT((int)m.Remove(key), (int)had);
|
|
ref.erase(key);
|
|
}
|
|
if ((int)m.size() != (int)ref.size()) {
|
|
printf("Hashmap size diverged at iteration %d: %d vs %d\n", i, (int)m.size(), (int)ref.size());
|
|
return false;
|
|
}
|
|
}
|
|
|
|
// Every key the reference has, the map must have - with the same value - and nothing else.
|
|
for (const auto &pair : ref) {
|
|
void *v = nullptr;
|
|
if (!m.Get(pair.first, &v) || v != pair.second) {
|
|
printf("Hashmap lost key %u\n", pair.first);
|
|
return false;
|
|
}
|
|
}
|
|
int seen = 0;
|
|
m.Iterate([&](const uint32_t &key, void *value) {
|
|
seen++;
|
|
if (ref.find(key) == ref.end())
|
|
printf("Hashmap has phantom key %u\n", key);
|
|
});
|
|
EXPECT_EQ_INT(seen, (int)ref.size());
|
|
}
|
|
|
|
// Insert/remove churn with fresh keys every round leaves tombstones behind. They take up
|
|
// probe slots exactly like real entries do, so if they aren't counted towards the load factor
|
|
// the table fills up with them - and then a lookup for a missing key never finds a FREE bucket
|
|
// to stop at. The map stays small the whole time, so nothing here should be slow or fail.
|
|
{
|
|
DenseHashMap<uint32_t, void *> m(16);
|
|
for (int round = 0; round < 200; round++) {
|
|
for (int i = 0; i < 4; i++)
|
|
EXPECT_TRUE(m.Insert(round * 4 + i, HashValue(i)));
|
|
for (int i = 0; i < 4; i++)
|
|
EXPECT_TRUE(m.Remove(round * 4 + i));
|
|
EXPECT_EQ_INT((int)m.size(), 0);
|
|
// A miss has to terminate. If tombstones have eaten every bucket, this is where a
|
|
// linear-probing map spins forever.
|
|
void *v = nullptr;
|
|
EXPECT_FALSE(m.Get(0xD1A6, &v));
|
|
}
|
|
}
|
|
|
|
// PrehashMap is the same structure keyed directly on a precomputed hash.
|
|
{
|
|
PrehashMap<void *> m(16);
|
|
EXPECT_TRUE(m.Insert(0x1000, HashValue(1)));
|
|
EXPECT_TRUE(m.Insert(0x2000, HashValue(2)));
|
|
// It reports a duplicate rather than asserting, unlike DenseHashMap.
|
|
EXPECT_FALSE(m.Insert(0x1000, HashValue(3)));
|
|
|
|
void *v = nullptr;
|
|
EXPECT_TRUE(m.Get(0x1000, &v));
|
|
EXPECT_TRUE(v == HashValue(1));
|
|
EXPECT_FALSE(m.Get(0x3000, &v));
|
|
EXPECT_TRUE(m.Remove(0x1000));
|
|
EXPECT_FALSE(m.Get(0x1000, &v));
|
|
EXPECT_TRUE(m.Get(0x2000, &v));
|
|
|
|
// Same tombstone churn as above.
|
|
for (int round = 0; round < 200; round++) {
|
|
for (int i = 0; i < 4; i++)
|
|
EXPECT_TRUE(m.Insert(0x10000 + round * 4 + i, HashValue(i)));
|
|
for (int i = 0; i < 4; i++)
|
|
EXPECT_TRUE(m.Remove(0x10000 + round * 4 + i));
|
|
EXPECT_FALSE(m.Get(0xD1A6, &v));
|
|
}
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
bool TestTinySet() {
|
|
TinySet<int, 4> a;
|
|
EXPECT_EQ_INT((int)a.size(), 0);
|
|
a.push_back(1);
|
|
EXPECT_EQ_INT((int)a.size(), 1);
|
|
a.push_back(2);
|
|
EXPECT_EQ_INT((int)a.size(), 2);
|
|
TinySet<int, 4> b;
|
|
b.push_back(8);
|
|
b.push_back(9);
|
|
b.push_back(10);
|
|
EXPECT_EQ_INT((int)b.size(), 3);
|
|
|
|
a.append(b);
|
|
EXPECT_EQ_INT((int)a.size(), 5);
|
|
EXPECT_EQ_INT((int)b.size(), 3);
|
|
|
|
b.append(b);
|
|
EXPECT_EQ_INT((int)b.size(), 6);
|
|
|
|
EXPECT_EQ_INT(a[0], 1);
|
|
EXPECT_EQ_INT(a[1], 2);
|
|
EXPECT_EQ_INT(a[2], 8);
|
|
EXPECT_EQ_INT(a[3], 9);
|
|
EXPECT_EQ_INT(a[4], 10);
|
|
a.append(a);
|
|
EXPECT_EQ_INT(a.size(), 10);
|
|
EXPECT_EQ_INT(a[9], 10);
|
|
|
|
b.push_back(11);
|
|
EXPECT_EQ_INT((int)b.size(), 7);
|
|
b.push_back(12);
|
|
EXPECT_EQ_INT((int)b.size(), 8);
|
|
b.push_back(13);
|
|
EXPECT_EQ_INT(b.size(), 9);
|
|
return true;
|
|
}
|
|
|
|
bool TestFastVec() {
|
|
FastVec<int> a;
|
|
EXPECT_EQ_INT((int)a.size(), 0);
|
|
a.push_back(1);
|
|
EXPECT_EQ_INT((int)a.size(), 1);
|
|
a.push_back(2);
|
|
EXPECT_EQ_INT((int)a.size(), 2);
|
|
FastVec<int> b;
|
|
b.push_back(8);
|
|
b.push_back(9);
|
|
b.push_back(10);
|
|
EXPECT_EQ_INT((int)b.size(), 3);
|
|
for (int i = 0; i < 100; i++) {
|
|
b.push_back(33);
|
|
}
|
|
EXPECT_EQ_INT((int)b.size(), 103);
|
|
|
|
int items[4] = { 50, 60, 70, 80 };
|
|
b.insert(b.begin() + 1, items, items + 4);
|
|
EXPECT_EQ_INT(b[0], 8);
|
|
EXPECT_EQ_INT(b[1], 50);
|
|
EXPECT_EQ_INT(b[2], 60);
|
|
EXPECT_EQ_INT(b[3], 70);
|
|
EXPECT_EQ_INT(b[4], 80);
|
|
EXPECT_EQ_INT(b[5], 9);
|
|
|
|
b.resize(2);
|
|
b.insert(b.end(), items, items + 4);
|
|
EXPECT_EQ_INT(b[0], 8);
|
|
EXPECT_EQ_INT(b[1], 50);
|
|
EXPECT_EQ_INT(b[2], 50);
|
|
EXPECT_EQ_INT(b[3], 60);
|
|
EXPECT_EQ_INT(b[4], 70);
|
|
EXPECT_EQ_INT(b[5], 80);
|
|
|
|
|
|
return true;
|
|
}
|
|
|
|
bool TestVFPUSinCos() {
|
|
float sine, cosine;
|
|
// Needed for VFPU tables.
|
|
// There might be a better place to invoke it, but whatever.
|
|
g_VFS.Register("", new DirectoryReader(Path("assets")));
|
|
InitVFPU();
|
|
vfpu_sincos(0.0f, sine, cosine);
|
|
EXPECT_EQ_FLOAT(sine, 0.0f);
|
|
EXPECT_EQ_FLOAT(cosine, 1.0f);
|
|
vfpu_sincos(1.0f, sine, cosine);
|
|
EXPECT_APPROX_EQ_FLOAT(sine, 1.0f);
|
|
EXPECT_APPROX_EQ_FLOAT(cosine, 0.0f);
|
|
vfpu_sincos(2.0f, sine, cosine);
|
|
EXPECT_APPROX_EQ_FLOAT(sine, 0.0f);
|
|
EXPECT_APPROX_EQ_FLOAT(cosine, -1.0f);
|
|
vfpu_sincos(3.0f, sine, cosine);
|
|
EXPECT_APPROX_EQ_FLOAT(sine, -1.0f);
|
|
EXPECT_APPROX_EQ_FLOAT(cosine, 0.0f);
|
|
vfpu_sincos(4.0f, sine, cosine);
|
|
EXPECT_EQ_FLOAT(sine, 0.0f);
|
|
EXPECT_EQ_FLOAT(cosine, 1.0f);
|
|
vfpu_sincos(5.0f, sine, cosine);
|
|
EXPECT_APPROX_EQ_FLOAT(sine, 1.0f);
|
|
EXPECT_APPROX_EQ_FLOAT(cosine, 0.0f);
|
|
|
|
vfpu_sincos(-1.0f, sine, cosine);
|
|
EXPECT_EQ_FLOAT(sine, -1.0f);
|
|
EXPECT_EQ_FLOAT(cosine, 0.0f);
|
|
vfpu_sincos(-2.0f, sine, cosine);
|
|
EXPECT_EQ_FLOAT(sine, 0.0f);
|
|
EXPECT_EQ_FLOAT(cosine, -1.0f);
|
|
|
|
for (float angle = -10.0f; angle < 10.0f; angle += 0.1f) {
|
|
vfpu_sincos(angle, sine, cosine);
|
|
EXPECT_APPROX_EQ_FLOAT(sine, sinf(angle * M_PI_2));
|
|
EXPECT_APPROX_EQ_FLOAT(cosine, cosf(angle * M_PI_2));
|
|
|
|
if (g_testLog) {
|
|
printf("sine: %f==%f cosine: %f==%f\n", sine, sinf(angle * M_PI_2), cosine, cosf(angle * M_PI_2));
|
|
}
|
|
}
|
|
return true;
|
|
}
|
|
|
|
bool TestVFPUMatrixTranspose() {
|
|
MatrixSize sz = M_4x4;
|
|
int matrix = 0; // M000
|
|
u8 cols[4];
|
|
u8 rows[4];
|
|
|
|
GetMatrixColumns(matrix, sz, cols);
|
|
GetMatrixRows(matrix, sz, rows);
|
|
|
|
int transposed = Xpose(matrix);
|
|
u8 x_cols[4];
|
|
u8 x_rows[4];
|
|
|
|
GetMatrixColumns(transposed, sz, x_cols);
|
|
GetMatrixRows(transposed, sz, x_rows);
|
|
|
|
for (int i = 0; i < GetMatrixSide(sz); i++) {
|
|
EXPECT_EQ_INT(cols[i], x_rows[i]);
|
|
EXPECT_EQ_INT(x_cols[i], rows[i]);
|
|
}
|
|
return true;
|
|
}
|
|
|
|
// TODO: Hook this up again!
|
|
void TestGetMatrix(int matrix, MatrixSize sz) {
|
|
INFO_LOG(Log::System, "Testing matrix %s", GetMatrixNotation(matrix, sz).c_str());
|
|
u8 fullMatrix[16];
|
|
|
|
u8 cols[4];
|
|
u8 rows[4];
|
|
|
|
GetMatrixColumns(matrix, sz, cols);
|
|
GetMatrixRows(matrix, sz, rows);
|
|
|
|
GetMatrixRegs(fullMatrix, sz, matrix);
|
|
|
|
int n = GetMatrixSide(sz);
|
|
VectorSize vsz = GetVectorSize(sz);
|
|
for (int i = 0; i < n; i++) {
|
|
// int colName = GetColumnName(matrix, sz, i, 0);
|
|
// int rowName = GetRowName(matrix, sz, i, 0);
|
|
int colName = cols[i];
|
|
int rowName = rows[i];
|
|
INFO_LOG(Log::System, "Column %i: %s", i, GetVectorNotation(colName, vsz).c_str());
|
|
INFO_LOG(Log::System, "Row %i: %s", i, GetVectorNotation(rowName, vsz).c_str());
|
|
|
|
u8 colRegs[4];
|
|
u8 rowRegs[4];
|
|
GetVectorRegs(colRegs, vsz, colName);
|
|
GetVectorRegs(rowRegs, vsz, rowName);
|
|
|
|
// Check that the individual regs are the expected ones.
|
|
std::stringstream a, b, c, d;
|
|
for (int j = 0; j < n; j++) {
|
|
a.clear();
|
|
b.clear();
|
|
a << (int)fullMatrix[i * 4 + j] << " ";
|
|
b << (int)colRegs[j] << " ";
|
|
|
|
c.clear();
|
|
d.clear();
|
|
|
|
c << (int)fullMatrix[j * 4 + i] << " ";
|
|
d << (int)rowRegs[j] << " ";
|
|
}
|
|
INFO_LOG(Log::System, "Col: %s vs %s", a.str().c_str(), b.str().c_str());
|
|
if (a.str() != b.str())
|
|
INFO_LOG(Log::System, "WRONG!");
|
|
INFO_LOG(Log::System, "Row: %s vs %s", c.str().c_str(), d.str().c_str());
|
|
if (c.str() != d.str())
|
|
INFO_LOG(Log::System, "WRONG!");
|
|
}
|
|
}
|
|
|
|
bool TestParseLBN() {
|
|
const char *validStrings[] = {
|
|
"/sce_lbn0x5fa0_size0x1428",
|
|
"/sce_lbn7050_sizeee850",
|
|
"/sce_lbn0x5eeeh_size0x234x", // Check for trailing chars. See #7960.
|
|
"/sce_lbneee__size434.", // Check for trailing chars. See #7960.
|
|
};
|
|
int expectedResults[][2] = {
|
|
{0x5fa0, 0x1428},
|
|
{0x7050, 0xee850},
|
|
{0x5eee, 0x234},
|
|
{0xeee, 0x434},
|
|
};
|
|
const char *invalidStrings[] = {
|
|
"/sce_lbn0x5fa0_sze0x1428",
|
|
"",
|
|
"//",
|
|
};
|
|
for (int i = 0; i < ARRAY_SIZE(validStrings); i++) {
|
|
u32 startSector = 0, readSize = 0;
|
|
// printf("testing %s\n", validStrings[i]);
|
|
EXPECT_TRUE(parseLBN(validStrings[i], &startSector, &readSize));
|
|
EXPECT_EQ_INT(startSector, expectedResults[i][0]);
|
|
EXPECT_EQ_INT(readSize, expectedResults[i][1]);
|
|
}
|
|
for (int i = 0; i < ARRAY_SIZE(invalidStrings); i++) {
|
|
u32 startSector, readSize;
|
|
EXPECT_FALSE(parseLBN(invalidStrings[i], &startSector, &readSize));
|
|
}
|
|
return true;
|
|
}
|
|
|
|
// So we can use EXPECT_TRUE, etc.
|
|
struct AlignedMem {
|
|
AlignedMem(size_t sz, size_t alignment = 16) {
|
|
p_ = AllocateAlignedMemory(sz, alignment);
|
|
}
|
|
~AlignedMem() {
|
|
FreeAlignedMemory(p_);
|
|
}
|
|
|
|
operator void *() {
|
|
return p_;
|
|
}
|
|
|
|
operator char *() {
|
|
return (char *)p_;
|
|
}
|
|
|
|
private:
|
|
void *p_;
|
|
};
|
|
|
|
bool TestQuickTexHash() {
|
|
static const int BUF_SIZE = 1024;
|
|
AlignedMem buf(BUF_SIZE, 16);
|
|
|
|
memset(buf, 0, BUF_SIZE);
|
|
EXPECT_EQ_HEX(StableQuickTexHash(buf, BUF_SIZE), 0xaa756edc);
|
|
|
|
memset(buf, 1, BUF_SIZE);
|
|
EXPECT_EQ_HEX(StableQuickTexHash(buf, BUF_SIZE), 0x66f81b1c);
|
|
|
|
strncpy(buf, "hello", BUF_SIZE);
|
|
EXPECT_EQ_HEX(StableQuickTexHash(buf, BUF_SIZE), 0xf6028131);
|
|
|
|
strncpy(buf, "goodbye", BUF_SIZE);
|
|
EXPECT_EQ_HEX(StableQuickTexHash(buf, BUF_SIZE), 0xef81b54f);
|
|
|
|
// Simple patterns.
|
|
for (int i = 0; i < BUF_SIZE; ++i) {
|
|
char *p = buf;
|
|
p[i] = i & 0xFF;
|
|
}
|
|
EXPECT_EQ_HEX(StableQuickTexHash(buf, BUF_SIZE), 0x0d64531c);
|
|
|
|
int j = 573;
|
|
for (int i = 0; i < BUF_SIZE; ++i) {
|
|
char *p = buf;
|
|
j += ((i * 7) + (i & 3)) * 11;
|
|
p[i] = j & 0xFF;
|
|
}
|
|
EXPECT_EQ_HEX(StableQuickTexHash(buf, BUF_SIZE), 0x58de8dbc);
|
|
|
|
return true;
|
|
}
|
|
|
|
bool TestCLZ() {
|
|
static const uint32_t input[] = {
|
|
0xFFFFFFFF,
|
|
0x00FFFFF0,
|
|
0x00101000,
|
|
0x00003000,
|
|
0x00000001,
|
|
0x00000000,
|
|
};
|
|
static const uint32_t expected[] = {
|
|
0,
|
|
8,
|
|
11,
|
|
18,
|
|
31,
|
|
32,
|
|
};
|
|
for (int i = 0; i < ARRAY_SIZE(input); i++) {
|
|
EXPECT_EQ_INT(clz32(input[i]), expected[i]);
|
|
}
|
|
return true;
|
|
}
|
|
|
|
static bool TestMemMap() {
|
|
Memory::g_MemorySize = Memory::RAM_DOUBLE_SIZE;
|
|
|
|
enum class Flags {
|
|
NO_KERNEL = 0,
|
|
ALLOW_KERNEL = 1,
|
|
};
|
|
struct Range {
|
|
uint32_t base;
|
|
uint32_t size;
|
|
Flags flags;
|
|
};
|
|
static const Range ranges[] = {
|
|
{ 0x08000000, Memory::RAM_DOUBLE_SIZE, Flags::ALLOW_KERNEL },
|
|
{ 0x00010000, Memory::SCRATCHPAD_SIZE, Flags::NO_KERNEL },
|
|
{ 0x04000000, 0x00800000, Flags::NO_KERNEL }, // VRAM (although we don't take wrapping into account here...)
|
|
};
|
|
static const uint32_t extraBits[] = {
|
|
0x00000000,
|
|
0x40000000,
|
|
0x80000000,
|
|
};
|
|
|
|
for (const auto &range : ranges) {
|
|
size_t testBits = range.flags == Flags::ALLOW_KERNEL ? 3 : 2;
|
|
for (size_t i = 0; i < testBits; ++i) {
|
|
uint32_t base = range.base | extraBits[i];
|
|
|
|
EXPECT_TRUE(Memory::IsValidAddress(base));
|
|
EXPECT_TRUE(Memory::IsValidAddress(base + range.size - 1));
|
|
EXPECT_FALSE(Memory::IsValidAddress(base + range.size));
|
|
EXPECT_FALSE(Memory::IsValidAddress(base - 1));
|
|
|
|
EXPECT_EQ_HEX(Memory::ClampValidSizeAt(base, range.size), range.size);
|
|
EXPECT_EQ_HEX(Memory::ClampValidSizeAt(base, range.size + 1), range.size);
|
|
EXPECT_EQ_HEX(Memory::ClampValidSizeAt(base, range.size - 1), range.size - 1);
|
|
EXPECT_EQ_HEX(Memory::ClampValidSizeAt(base, 0), 0);
|
|
EXPECT_EQ_HEX(Memory::ClampValidSizeAt(base, 0x80000001), range.size);
|
|
EXPECT_EQ_HEX(Memory::ClampValidSizeAt(base, 0x40000001), range.size);
|
|
EXPECT_EQ_HEX(Memory::ClampValidSizeAt(base, 0x20000001), range.size);
|
|
EXPECT_EQ_HEX(Memory::ClampValidSizeAt(base, 0x10000001), range.size);
|
|
|
|
EXPECT_EQ_HEX(Memory::ClampValidSizeAt(base + range.size - 0x10, 0x20000001), 0x10);
|
|
}
|
|
}
|
|
|
|
EXPECT_FALSE(Memory::IsValidAddress(0x00015000));
|
|
EXPECT_FALSE(Memory::IsValidAddress(0x04900000));
|
|
EXPECT_EQ_HEX(Memory::ClampValidSizeAt(0x00015000, 4), 0);
|
|
EXPECT_EQ_HEX(Memory::ClampValidSizeAt(0x04900000, 4), 0);
|
|
|
|
return true;
|
|
}
|
|
|
|
static bool TestPath() {
|
|
// Also test the Path class while we're at it.
|
|
Path path("/asdf/jkl/");
|
|
EXPECT_EQ_STR(path.ToString(), std::string("/asdf/jkl"));
|
|
|
|
Path path2("/asdf/jkl");
|
|
EXPECT_EQ_STR(path2.NavigateUp().ToString(), std::string("/asdf"));
|
|
|
|
Path path3 = path2 / "foo/bar";
|
|
EXPECT_EQ_STR(path3.WithExtraExtension(".txt").ToString(), std::string("/asdf/jkl/foo/bar.txt"));
|
|
|
|
EXPECT_EQ_STR(Path("foo.bar/hello").GetFileExtension(), std::string());
|
|
EXPECT_EQ_STR(Path("foo.bar/hello.txt").WithReplacedExtension(".txt", ".html").ToString(), std::string("foo.bar/hello.html"));
|
|
|
|
EXPECT_EQ_STR(Path("C:\\Yo").NavigateUp().ToString(), std::string("C:"));
|
|
#if PPSSPP_PLATFORM(WINDOWS)
|
|
EXPECT_EQ_STR(Path("C:").NavigateUp().ToString(), std::string("/"));
|
|
|
|
EXPECT_EQ_STR(Path("C:\\Yo").GetDirectory(), std::string("C:"));
|
|
EXPECT_EQ_STR(Path("C:\\Yo").GetFilename(), std::string("Yo"));
|
|
EXPECT_EQ_STR(Path("C:\\Yo\\Lo").GetDirectory(), std::string("C:/Yo"));
|
|
EXPECT_EQ_STR(Path("C:\\Yo\\Lo").GetFilename(), std::string("Lo"));
|
|
|
|
EXPECT_EQ_STR(Path(R"(\\host\share\filename)").GetRootVolume().ToString(), std::string("//host"));
|
|
EXPECT_EQ_STR(Path(R"(\\?\UNC\share\filename)").GetRootVolume().ToString(), std::string("//?/UNC"));
|
|
EXPECT_EQ_STR(Path(R"(\\?\C:\share\filename)").GetRootVolume().ToString(), std::string("//?/C:"));
|
|
#endif
|
|
|
|
std::string computedPath;
|
|
|
|
EXPECT_TRUE(Path("/a/b").ComputePathTo(Path("/a/b/c/d/e"), computedPath));
|
|
|
|
EXPECT_EQ_STR(computedPath, std::string("c/d/e"));
|
|
|
|
EXPECT_TRUE(Path("/").ComputePathTo(Path("/home/foo/bar"), computedPath));
|
|
EXPECT_EQ_STR(computedPath, std::string("home/foo/bar"));
|
|
|
|
EXPECT_TRUE(Path("/a/b").ComputePathTo(Path("/a/b"), computedPath));
|
|
EXPECT_EQ_STR(computedPath, std::string());
|
|
|
|
return true;
|
|
}
|
|
|
|
static bool TestAndroidContentURI() {
|
|
static const char *treeURIString = "content://com.android.externalstorage.documents/tree/primary%3APSP%20ISO";
|
|
static const char *directoryURIString = "content://com.android.externalstorage.documents/tree/primary%3APSP%20ISO/document/primary%3APSP%20ISO";
|
|
static const char *fileTreeURIString = "content://com.android.externalstorage.documents/tree/primary%3APSP%20ISO/document/primary%3APSP%20ISO%2FTekken%206.iso";
|
|
static const char *fileNonTreeString = "content://com.android.externalstorage.documents/document/primary%3APSP%2Fcrash_bad_execaddr.prx";
|
|
static const char *downloadURIString = "content://com.android.providers.downloads.documents/document/msf%3A10000000006";
|
|
|
|
AndroidContentURI treeURI;
|
|
EXPECT_TRUE(treeURI.Parse(treeURIString));
|
|
AndroidContentURI dirURI;
|
|
EXPECT_TRUE(dirURI.Parse(directoryURIString));
|
|
AndroidContentURI fileTreeURI;
|
|
EXPECT_TRUE(fileTreeURI.Parse(fileTreeURIString));
|
|
AndroidContentURI fileTreeURICopy;
|
|
EXPECT_TRUE(fileTreeURICopy.Parse(fileTreeURIString));
|
|
AndroidContentURI fileURI;
|
|
EXPECT_TRUE(fileURI.Parse(fileNonTreeString));
|
|
|
|
EXPECT_EQ_STR(fileTreeURI.GetLastPart(), std::string("Tekken 6.iso"));
|
|
|
|
EXPECT_TRUE(treeURI.TreeContains(fileTreeURI));
|
|
|
|
EXPECT_TRUE(fileTreeURI.CanNavigateUp());
|
|
fileTreeURI.NavigateUp();
|
|
EXPECT_FALSE(fileTreeURI.CanNavigateUp());
|
|
|
|
EXPECT_EQ_STR(fileTreeURI.FilePath(), fileTreeURI.RootPath());
|
|
|
|
EXPECT_EQ_STR(fileTreeURI.ToString(), std::string(directoryURIString));
|
|
|
|
std::string diff;
|
|
EXPECT_TRUE(dirURI.ComputePathTo(fileTreeURICopy, diff));
|
|
EXPECT_EQ_STR(diff, std::string("Tekken 6.iso"));
|
|
|
|
EXPECT_EQ_STR(fileURI.GetFileExtension(), std::string(".prx"));
|
|
EXPECT_TRUE(fileURI.CanNavigateUp()); // Can now virtually navigate up one step from these.
|
|
|
|
// These are annoying because they hide the actual filename, and we can't get at a parent folder.
|
|
// Decided to handle the ':' as a directory separator for navigation purposes, which fixes the problem (though not the extension thing).
|
|
AndroidContentURI downloadURI;
|
|
EXPECT_TRUE(downloadURI.Parse(std::string(downloadURIString)));
|
|
EXPECT_EQ_STR(downloadURI.GetLastPart(), std::string("10000000006"));
|
|
EXPECT_TRUE(downloadURI.CanNavigateUp());
|
|
EXPECT_TRUE(downloadURI.NavigateUp());
|
|
// While this is not an openable valid content URI, we can still get something that we can concatenate a filename on top of.
|
|
EXPECT_EQ_STR(downloadURI.ToString(), std::string("content://com.android.providers.downloads.documents/document/msf%3A"));
|
|
EXPECT_EQ_STR(downloadURI.GetLastPart(), std::string("msf:"));
|
|
downloadURI = downloadURI.WithComponent("myfile");
|
|
EXPECT_EQ_STR(downloadURI.ToString(), std::string("content://com.android.providers.downloads.documents/document/msf%3Amyfile"));
|
|
return true;
|
|
}
|
|
|
|
class UnitTestWordWrapper : public WordWrapper {
|
|
public:
|
|
UnitTestWordWrapper(std::string_view str, float maxW, int flags)
|
|
: WordWrapper(str, maxW, flags) {
|
|
}
|
|
|
|
protected:
|
|
float MeasureWidth(std::string_view str) override {
|
|
// Simple case for unit testing.
|
|
int w = 0;
|
|
for (UTF8 utf(str); !utf.end(); ) {
|
|
uint32_t c = utf.next();
|
|
switch (c) {
|
|
case ' ':
|
|
case '.':
|
|
w += 1;
|
|
break;
|
|
case 0x00AD:
|
|
// No width for soft hyphens.
|
|
break;
|
|
default:
|
|
w += 2;
|
|
break;
|
|
}
|
|
}
|
|
|
|
return w;
|
|
}
|
|
};
|
|
|
|
#define EXPECT_WORDWRAP_EQ_STR(a, l, f, b) if (UnitTestWordWrapper(a, l, f).Wrapped() != b) { printf("%s: Test Fail (%d, %s)\n%s\nvs\n%s\n", __FUNCTION__, l, #f, UnitTestWordWrapper(a, l, f).Wrapped().c_str(), std::string(b).c_str()); return false; }
|
|
|
|
static bool TestWrapText() {
|
|
// If there's enough space, it shouldn't wrap. This is exactly enough.
|
|
EXPECT_WORDWRAP_EQ_STR("Hello", 10, 0, "Hello");
|
|
EXPECT_WORDWRAP_EQ_STR("Hello", 10, FLAG_WRAP_TEXT, "Hello");
|
|
EXPECT_WORDWRAP_EQ_STR("Hello", 10, FLAG_ELLIPSIZE_TEXT, "Hello");
|
|
EXPECT_WORDWRAP_EQ_STR("Hello", 10, FLAG_WRAP_TEXT | FLAG_ELLIPSIZE_TEXT, "Hello");
|
|
|
|
// Try a single word that doesn't fit in the space.
|
|
EXPECT_WORDWRAP_EQ_STR("Hello", 6, 0, "Hello");
|
|
EXPECT_WORDWRAP_EQ_STR("Hello", 6, FLAG_WRAP_TEXT, "Hel\nlo");
|
|
EXPECT_WORDWRAP_EQ_STR("Hello", 6, FLAG_ELLIPSIZE_TEXT, "H...");
|
|
EXPECT_WORDWRAP_EQ_STR("Hello", 6, FLAG_WRAP_TEXT | FLAG_ELLIPSIZE_TEXT, "H...");
|
|
|
|
// Now, multiple words.
|
|
EXPECT_WORDWRAP_EQ_STR("Hello goodbye", 14, 0, "Hello goodbye");
|
|
EXPECT_WORDWRAP_EQ_STR("Hello goodbye", 14, FLAG_WRAP_TEXT, "Hello \ngoodbye");
|
|
EXPECT_WORDWRAP_EQ_STR("Hello goodbye", 14, FLAG_ELLIPSIZE_TEXT, "Hello...");
|
|
EXPECT_WORDWRAP_EQ_STR("Hello goodbye", 14, FLAG_WRAP_TEXT | FLAG_ELLIPSIZE_TEXT, "Hello \ngoodbye");
|
|
|
|
// Multiple words with something short after...
|
|
EXPECT_WORDWRAP_EQ_STR("Hello goodbye yes", 14, 0, "Hello goodbye ");
|
|
EXPECT_WORDWRAP_EQ_STR("Hello goodbye yes", 14, FLAG_WRAP_TEXT, "Hello \ngoodbye \nyes");
|
|
EXPECT_WORDWRAP_EQ_STR("Hello goodbye yes", 14, FLAG_ELLIPSIZE_TEXT, "Hello...");
|
|
EXPECT_WORDWRAP_EQ_STR("Hello goodbye yes", 14, FLAG_WRAP_TEXT | FLAG_ELLIPSIZE_TEXT, "Hello \ngoodbye \nyes");
|
|
|
|
// Now, multiple words, but only the first fits.
|
|
EXPECT_WORDWRAP_EQ_STR("Hello goodbye", 10, 0, "Hello ");
|
|
EXPECT_WORDWRAP_EQ_STR("Hello goodbye", 10, FLAG_WRAP_TEXT, "Hello \ngoodb\nye");
|
|
EXPECT_WORDWRAP_EQ_STR("Hello goodbye", 10, FLAG_ELLIPSIZE_TEXT, "Hel...");
|
|
EXPECT_WORDWRAP_EQ_STR("Hello goodbye", 10, FLAG_WRAP_TEXT | FLAG_ELLIPSIZE_TEXT, "Hello \ngoo...");
|
|
|
|
// How about the shy character?
|
|
const std::string shyTestString = StringFromFormat("Very%c%clong", 0xC2, 0xAD);
|
|
EXPECT_WORDWRAP_EQ_STR(shyTestString.c_str(), 10, 0, shyTestString);
|
|
EXPECT_WORDWRAP_EQ_STR(shyTestString.c_str(), 10, FLAG_WRAP_TEXT, "Very-\nlong");
|
|
EXPECT_WORDWRAP_EQ_STR(shyTestString.c_str(), 10, FLAG_ELLIPSIZE_TEXT, "Very...");
|
|
EXPECT_WORDWRAP_EQ_STR(shyTestString.c_str(), 10, FLAG_WRAP_TEXT | FLAG_ELLIPSIZE_TEXT, "Very-\nlong");
|
|
|
|
// Newlines should not be removed and should influence wrapping.
|
|
EXPECT_WORDWRAP_EQ_STR("Hello\ngoodbye yes\nno", 14, 0, "Hello\ngoodbye ");
|
|
EXPECT_WORDWRAP_EQ_STR("Hello\ngoodbye yes\nno", 14, FLAG_WRAP_TEXT, "Hello\ngoodbye \nyes\nno");
|
|
EXPECT_WORDWRAP_EQ_STR("Hello\ngoodbye yes\nno", 14, FLAG_ELLIPSIZE_TEXT, "Hello\ngoodb...\nno");
|
|
EXPECT_WORDWRAP_EQ_STR("Hello\ngoodbye yes\nno", 14, FLAG_WRAP_TEXT | FLAG_ELLIPSIZE_TEXT, "Hello\ngoodbye \nyes\nno");
|
|
|
|
return true;
|
|
}
|
|
|
|
static bool TestSmallDataConvert() {
|
|
float f[4] = { 1.0f / 255.0f, 2.0f / 255.0f, 3.0f / 255.0f, 4.0f / 255.f };
|
|
uint32_t result = Float4ToUint8x4_NoClamp(f);
|
|
EXPECT_EQ_HEX(result, 0x04030201);
|
|
result = Float4ToUint8x4(f);
|
|
EXPECT_EQ_HEX(result, 0x04030201);
|
|
return true;
|
|
}
|
|
|
|
bool TestInputMapping() {
|
|
InputMapping mapping;
|
|
mapping.deviceId = DEVICE_ID_PAD_0;
|
|
mapping.keyCode = 20;
|
|
InputMapping mapping2;
|
|
mapping2.deviceId = DEVICE_ID_PAD_8;
|
|
mapping2.keyCode = 38;
|
|
std::string cfg = mapping.ToConfigString();
|
|
|
|
InputMapping parsedMapping = InputMapping::FromConfigString(cfg);
|
|
EXPECT_EQ_INT(parsedMapping.deviceId, mapping.deviceId);
|
|
EXPECT_EQ_INT(parsedMapping.keyCode, mapping.keyCode);
|
|
|
|
using KeyMap::MultiInputMapping;
|
|
MultiInputMapping multi(mapping);
|
|
|
|
EXPECT_EQ_STR(multi.ToConfigString(), mapping.ToConfigString());
|
|
|
|
multi.mappings.push_back(mapping2);
|
|
EXPECT_FALSE(multi.EqualsSingleMapping(mapping));
|
|
EXPECT_TRUE(multi.mappings.contains(mapping2));
|
|
EXPECT_TRUE(multi.mappings.contains(mapping));
|
|
|
|
std::string cfgMulti = multi.ToConfigString();
|
|
|
|
EXPECT_EQ_STR(cfgMulti, std::string("10-20:18-38"));
|
|
|
|
MultiInputMapping parsedMulti = MultiInputMapping::FromConfigString(cfgMulti);
|
|
|
|
EXPECT_EQ_INT((int)parsedMulti.mappings.size(), 2);
|
|
|
|
// OK, both single and multiple mappings parse. Let's now see if the old parsing can handle a multimapping.
|
|
// This is a requirement for the new format.
|
|
|
|
InputMapping parsedMultiSingle = InputMapping::FromConfigString(cfgMulti); // yes this is an intentional mismatch
|
|
// We should get the first mapping.
|
|
EXPECT_TRUE(parsedMultiSingle == mapping);
|
|
return true;
|
|
}
|
|
|
|
bool TestEscapeMenuString() {
|
|
char c;
|
|
std::string temp = UnescapeMenuString("&File", &c);
|
|
EXPECT_EQ_INT((int)c, (int)'F');
|
|
EXPECT_EQ_STR(temp, std::string("File"));
|
|
temp = UnescapeMenuString("U&til", &c);
|
|
EXPECT_EQ_INT((int)c, (int)'t');
|
|
EXPECT_EQ_STR(temp, std::string("Util"));
|
|
temp = UnescapeMenuString("Ed&it", nullptr);
|
|
EXPECT_EQ_STR(temp, std::string("Edit"));
|
|
temp = UnescapeMenuString("Cut && Paste", nullptr);
|
|
EXPECT_EQ_STR(temp, std::string("Cut & Paste"));
|
|
temp = UnescapeMenuString("&A&B", &c);
|
|
EXPECT_EQ_STR(temp, std::string("AB"));
|
|
EXPECT_EQ_INT((int)c, (int)'A');
|
|
return true;
|
|
}
|
|
|
|
bool TestSubstitutions() {
|
|
std::string output = ApplySafeSubstitutions("%3 %2 %1", "a", "b", "c");
|
|
EXPECT_EQ_STR(output, std::string("c b a"));
|
|
return true;
|
|
}
|
|
|
|
bool TestIniFile() {
|
|
const std::string testLine = "adsf\\#asdf = jkl\\# # comment";
|
|
const std::string testLine2 = "# Just a comment";
|
|
|
|
std::string temp;
|
|
ParsedIniLine line(testLine);
|
|
line.Reconstruct(&temp);
|
|
EXPECT_EQ_STR(testLine, temp);
|
|
|
|
temp.clear();
|
|
ParsedIniLine line2(testLine2);
|
|
line2.Reconstruct(&temp);
|
|
|
|
EXPECT_EQ_STR(testLine2, temp);
|
|
return true;
|
|
}
|
|
|
|
inline u32 ReferenceRGBA5551ToRGBA8888(u16 src) {
|
|
u8 r = Convert5To8((src >> 0) & 0x1F);
|
|
u8 g = Convert5To8((src >> 5) & 0x1F);
|
|
u8 b = Convert5To8((src >> 10) & 0x1F);
|
|
u8 a = (src >> 15) & 0x1;
|
|
a = (a) ? 0xff : 0;
|
|
return (a << 24) | (b << 16) | (g << 8) | r;
|
|
}
|
|
|
|
inline u32 ReferenceRGB565ToRGBA8888(u16 src) {
|
|
u8 r = Convert5To8((src >> 0) & 0x1F);
|
|
u8 g = Convert6To8((src >> 5) & 0x3F);
|
|
u8 b = Convert5To8((src >> 11) & 0x1F);
|
|
u8 a = 0xFF;
|
|
return (a << 24) | (b << 16) | (g << 8) | r;
|
|
}
|
|
|
|
bool TestColorConv() {
|
|
// Can exhaustively test the 16->32 conversions.
|
|
for (int i = 0; i < 65536; i++) {
|
|
u16 col16 = i;
|
|
|
|
u32 reference = ReferenceRGBA5551ToRGBA8888(col16);
|
|
u32 value = RGBA5551ToRGBA8888(col16);
|
|
EXPECT_EQ_INT(reference, value);
|
|
|
|
reference = ReferenceRGB565ToRGBA8888(col16);
|
|
value = RGB565ToRGBA8888(col16);
|
|
EXPECT_EQ_INT(reference, value);
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
CharQueue GetQueue() {
|
|
CharQueue queue(5);
|
|
return queue;
|
|
}
|
|
|
|
bool TestCharQueue() {
|
|
// We use a tiny block size for testing.
|
|
CharQueue queue = GetQueue();
|
|
|
|
// Add 16 chars.
|
|
queue.push_back("abcdefghijkl");
|
|
queue.push_back("mnop");
|
|
|
|
std::string testStr;
|
|
queue.iterate_blocks([&](const char *buf, size_t sz) {
|
|
testStr.append(buf, sz);
|
|
return true;
|
|
});
|
|
EXPECT_EQ_STR(testStr, std::string("abcdefghijklmnop"));
|
|
|
|
EXPECT_EQ_CHAR(queue.peek(11), 'l');
|
|
EXPECT_EQ_CHAR(queue.peek(12), 'm');
|
|
EXPECT_EQ_CHAR(queue.peek(15), 'p');
|
|
EXPECT_EQ_INT(queue.block_count(), 3); // Didn't fit in the first block, so the two pushes above should have each created one additional block.
|
|
EXPECT_EQ_INT(queue.size(), 16);
|
|
char dest[15];
|
|
EXPECT_EQ_INT(queue.pop_front_bulk(dest, 4), 4);
|
|
EXPECT_EQ_INT(queue.size(), 12);
|
|
EXPECT_EQ_MEM(dest, "abcd", 4);
|
|
EXPECT_EQ_INT(queue.pop_front_bulk(dest, 6), 6);
|
|
EXPECT_EQ_INT(queue.size(), 6);
|
|
EXPECT_EQ_MEM(dest, "efghij", 6);
|
|
queue.push_back("qr");
|
|
EXPECT_EQ_INT(queue.pop_front_bulk(dest, 4), 4); // should pop off klmn
|
|
EXPECT_EQ_MEM(dest, "klmn", 4);
|
|
EXPECT_EQ_INT(queue.size(), 4);
|
|
EXPECT_EQ_CHAR(queue.peek(3), 'r');
|
|
queue.pop_front_bulk(dest, 4);
|
|
EXPECT_EQ_MEM(dest, "opqr", 4);
|
|
EXPECT_TRUE(queue.empty());
|
|
queue.push_back("asdf");
|
|
EXPECT_EQ_INT(queue.next_crlf_offset(), -1);
|
|
queue.push_back("\r\r\n");
|
|
EXPECT_EQ_INT(queue.next_crlf_offset(), 5);
|
|
return true;
|
|
}
|
|
|
|
bool TestBuffer() {
|
|
Buffer b = Buffer::Void();
|
|
b.Append("hello");
|
|
b.Append("world");
|
|
std::string temp;
|
|
b.Take(10, &temp);
|
|
EXPECT_EQ_STR(temp, std::string("helloworld"));
|
|
return true;
|
|
}
|
|
|
|
#if PPSSPP_ARCH(SSE2) && (defined(__GNUC__) || defined(__clang__) || defined(__INTEL_COMPILER))
|
|
[[gnu::target("sse4.1")]]
|
|
#endif
|
|
bool TestSIMD() {
|
|
#if PPSSPP_ARCH(SSE2)
|
|
__m128i x = _mm_set_epi16(0, 0x4444, 0, 0x3333, 0, 0x2222, 0, 0x1111);
|
|
__m128i y = _mm_packu_epi32_SSE2(x);
|
|
|
|
uint64_t testdata[2];
|
|
_mm_store_si128((__m128i *)testdata, y);
|
|
EXPECT_EQ_INT(testdata[0], 0x4444333322221111);
|
|
EXPECT_EQ_INT(testdata[1], 0);
|
|
|
|
__m128i a = _mm_set_epi16(0, 0x4444, 0, 0x3333, 0, 0x2222, 0, 0x1111);
|
|
__m128i b = _mm_set_epi16(0, (int16_t)0x8888, 0, 0x7777, 0, 0x6666, 0, 0x5555);
|
|
__m128i c = _mm_packu2_epi32_SSE2(a, b);
|
|
__m128i d = _mm_packu1_epi32_SSE2(b);
|
|
|
|
uint64_t testdata2[4];
|
|
_mm_store_si128((__m128i *)testdata2, c);
|
|
_mm_store_si128((__m128i *)testdata2 + 1, d);
|
|
EXPECT_EQ_INT(testdata2[0], 0x4444333322221111);
|
|
EXPECT_EQ_INT(testdata2[1], 0x8888777766665555);
|
|
EXPECT_EQ_INT(testdata2[2], 0x8888777766665555);
|
|
EXPECT_EQ_INT(testdata2[2], 0x8888777766665555);
|
|
#endif
|
|
|
|
const int testval[2][4] = {
|
|
{ 0x1000, 0x2000, 0x3000, 0x7000 },
|
|
{ -0x1000, -0x2000, -0x3000, -0x7000 }
|
|
};
|
|
|
|
for (int i = 0; i < 2; i++) {
|
|
Vec4S32 s = Vec4S32::Load(testval[i]);
|
|
Vec4S32 square = s * s;
|
|
Vec4S32 square16 = s.Mul16(s);
|
|
EXPECT_EQ_INT(square[0], square16[0]);
|
|
EXPECT_EQ_INT(square[1], square16[1]);
|
|
EXPECT_EQ_INT(square[2], square16[2]);
|
|
EXPECT_EQ_INT(square[3], square16[3]);
|
|
}
|
|
return true;
|
|
}
|
|
|
|
static void PrintFloats(const float *f, int count) {
|
|
for (int i = 0; i < count; i++) {
|
|
printf("%.1ff, ", f[i]);
|
|
}
|
|
printf("\n");
|
|
}
|
|
|
|
static bool CompareFloats(const float *values, const float *known_good, int count, int line) {
|
|
int wrongCount = 0;
|
|
|
|
for (int i = 0; i < count; i++) {
|
|
if (values[i] != known_good[i]) {
|
|
wrongCount++;
|
|
}
|
|
}
|
|
|
|
if (wrongCount > 0) {
|
|
for (int i = 0; i < count; i++) {
|
|
bool wrong = values[i] != known_good[i];
|
|
printf("%d: %0.3f vs %0.3f %s\n", i + 1, values[i], known_good[i], wrong ? "!! MISMATCH" : "");
|
|
}
|
|
printf("At UnitTest.cpp:%d: %d / %d were wrong\n", line, wrongCount, count);
|
|
return false;
|
|
} else {
|
|
return true;
|
|
}
|
|
}
|
|
|
|
bool TestCrossSIMD() {
|
|
static const float a_values[16] = { 1.0f, 2.0f, 3.0f, 4.0f, 5.0f, 7.0f, 8.0f, 9.0f, 10.0f, 11.0f, 12.0f, 13.0f, 14.0f, 15.0f };
|
|
static const float b_values[16] = { -12.0f, 3.0f, -2.5f, 5.0f, 31.0f, 0.5f, 4.0f, 6.0f, 7.0f, 13.0f, 12.0f, 51.0f, 81.0f, 32.0f };
|
|
static const float known_result[16] = { 395.0f, 171.0f, 41.5f, 170.0f, 942.0f, 410.5f, 111.5f, 475.0f, 1358.0f, 607.5f, 163.0f, 728.0f, 297.0f, 49.5f, 25.0f, 160.0f, };
|
|
float result[16];
|
|
Mat4F32 a(a_values);
|
|
Mat4F32 b(b_values);
|
|
|
|
Mul4x4By4x4(a, b).Store(result);
|
|
if (!CompareFloats(result, known_result, 16, __LINE__)) {
|
|
return false;
|
|
}
|
|
|
|
Mat4x3F32 d = Mat4x3F32(b_values + 2);
|
|
Mul4x3By4x4(d, a).Store(result);
|
|
|
|
static const float known_4x3_result[16] = { 332.5f, 371.0f, 404.5f, 438.0f, 80.5f, 95.0f, 105.5f, 116.0f, 192.0f, 237.0f, 269.0f, 301.0f, 790.0f, 1036.0f, 1185.0f, 1349.0f, };
|
|
if (!CompareFloats(result, known_4x3_result, 16, __LINE__)) {
|
|
return false;
|
|
}
|
|
|
|
static const float vec_values[4] = { 3.0f, 5.0f, 7.0f, 10000000.0f };
|
|
Vec4F32 v = Vec4F32::Load(vec_values);
|
|
|
|
v.AsVec3ByMatrix44(b).Store3(result);
|
|
|
|
static const float known_vec_result[3] = { 249.0f, 134.5f, 96.5f, };
|
|
if (!CompareFloats(result, known_vec_result, ARRAY_SIZE(known_vec_result), __LINE__)) {
|
|
return false;
|
|
}
|
|
Vec4F32 scale = Vec4F32::Load(a_values);
|
|
Vec4F32 translate = Vec4F32::Load(b_values);
|
|
|
|
TranslateAndScaleInplace(a, scale, translate);
|
|
a.Store(result);
|
|
|
|
static const float known_scale_result[16] = { -47.0f, 16.0f, -1.0f, 36.0f, -103.0f, 41.0f, 1.5f, 81.0f, -146.0f, 61.0f, 3.5f, 117.0f, 14.0f, 30.0f, 0.0f, 0.0f,};
|
|
if (!CompareFloats(result, known_scale_result, ARRAY_SIZE(known_scale_result), __LINE__)) {
|
|
return false;
|
|
}
|
|
|
|
s8 values[4] = {-1, -128, 127, 45};
|
|
float fvalues[4];
|
|
Vec4F32::LoadS8Norm(values).Store(fvalues);
|
|
static const float known_s8norm_result[4] = {(float)values[0]/128.0f, (float)values[1]/128.0f, (float)values[2]/128.0f, (float)values[3]/128.0f,};
|
|
if (!CompareFloats(fvalues, known_s8norm_result, ARRAY_SIZE(known_s8norm_result), __LINE__)) {
|
|
return false;
|
|
}
|
|
|
|
// PrintFloats(result, 16);
|
|
|
|
return true;
|
|
}
|
|
|
|
bool TestVolumeFunc() {
|
|
for (int i = 0; i <= 20; i++) {
|
|
float mul = Volume10ToMultiplier(i);
|
|
|
|
int vol100 = MultiplierToVolume100(mul);
|
|
float mul2 = Volume100ToMultiplier(vol100);
|
|
|
|
bool smaller = (fabsf(mul2 - mul) < 0.02f);
|
|
EXPECT_TRUE(smaller);
|
|
// printf("%d -> %f -> %d -> %f\n", i, mul, vol100, mul2);
|
|
}
|
|
return true;
|
|
}
|
|
|
|
bool TestLinAlg() {
|
|
static const float m1[16] = {
|
|
1, 2, 3, 4,
|
|
5, 6, 7, 8,
|
|
9, 10, 11, 12,
|
|
13, 14, 15, 16
|
|
};
|
|
static const float m2[16] = {
|
|
56, 0, 24, 2,
|
|
0.5f, 35, 2, 4,
|
|
1, 6, 1, 2,
|
|
4, 0, -1, -4
|
|
};
|
|
static const float correct[16] = {
|
|
298.f, 380.f, 462.f, 544.f,
|
|
245.5f, 287.f, 328.5f, 370.f,
|
|
66.f, 76.f, 86.f, 96.f,
|
|
-57.f, -58.f, -59.f, -60.f,
|
|
};
|
|
|
|
float d[16]{};
|
|
|
|
fast_matrix_mul_4x4(d, m1, m2);
|
|
|
|
for (int i = 0; i < 16; i += 4) {
|
|
// printf("%0.2f, %0.2f, %0.2f, %0.2f,\n", d[i], d[i + 1], d[i + 2], d[i + 3]);
|
|
}
|
|
|
|
for (int i = 0; i < 16; i++) {
|
|
EXPECT_EQ_FLOAT(d[i], correct[i]);
|
|
}
|
|
|
|
|
|
// OK, now test 4x3 multiplication.
|
|
float a4x4[16];
|
|
float b4x4[16];
|
|
|
|
ConvertMatrix4x3To4x4(a4x4, m1);
|
|
ConvertMatrix4x3To4x4(b4x4, m1);
|
|
Matrix4ByMatrix4(d, a4x4, b4x4);
|
|
|
|
for (int i = 0; i < 16; i += 4) {
|
|
// printf("%0.2f, %0.2f, %0.2f, %0.2f,\n", d[i], d[i + 1], d[i + 2], d[i + 3]);
|
|
}
|
|
|
|
static const float correct4x4[16] = {
|
|
30.00, 36.00, 42.00, 0.00,
|
|
66.00, 81.00, 96.00, 0.00,
|
|
102.00, 126.00, 150.00, 0.00,
|
|
148.00, 182.00, 216.00, 1.00,
|
|
};
|
|
|
|
for (int i = 0; i < 16; i++) {
|
|
EXPECT_EQ_FLOAT(d[i], correct4x4[i]);
|
|
}
|
|
|
|
ConvertMatrix4x3To4x4Transposed(b4x4, m1);
|
|
Matrix4ByMatrix4(d, a4x4, b4x4);
|
|
|
|
static const float correct4x4transposed[16] = {
|
|
14.00, 32.00, 50.00, 68.00,
|
|
32.00, 77.00, 122.00, 167.00,
|
|
50.00, 122.00, 194.00, 266.00,
|
|
68.00, 167.00, 266.00, 366.00,
|
|
};
|
|
|
|
for (int i = 0; i < 16; i++) {
|
|
EXPECT_EQ_FLOAT(d[i], correct4x4transposed[i]);
|
|
}
|
|
// TODO: Add direct 4x3 x 4x3 multiplication
|
|
return true;
|
|
}
|
|
|
|
bool TestSplitSearch() {
|
|
std::string part1 = "The quick brown fox jumps";
|
|
std::string part2 = " over the lazy dog.";
|
|
|
|
size_t offset = SplitSearch("jumps over", part1, part2);
|
|
EXPECT_EQ_INT(offset, 20);
|
|
offset = SplitSearch("quick", part1, part2);
|
|
EXPECT_EQ_INT(offset, 4);
|
|
offset = SplitSearch(" over", part1, part2);
|
|
EXPECT_EQ_INT(offset, 25);
|
|
offset = SplitSearch("fox jumps", part1, part2);
|
|
EXPECT_EQ_INT(offset, 16);
|
|
offset = SplitSearch("dog.", part1, part2);
|
|
EXPECT_EQ_INT(offset, 40);
|
|
return true;
|
|
}
|
|
|
|
bool TestFriendlyPath() {
|
|
Path path("/home/user/PPSSPP/games/My Game (USA)/EBOOT.PBP");
|
|
Path baseDir("/home/user/PPSSPP/games/");
|
|
std::string friendlyPath = GetFriendlyPath(path, baseDir, "ms:/");
|
|
EXPECT_EQ_STR(friendlyPath, std::string("ms:/My Game (USA)/EBOOT.PBP"));
|
|
return true;
|
|
}
|
|
|
|
bool TestCmdLine() {
|
|
{
|
|
const char *argv[] = {
|
|
"ppsspp",
|
|
"--fullscreen",
|
|
"--graphics=d3d11",
|
|
"--pause-menu-exit",
|
|
"My_Game.iso"
|
|
};
|
|
int argc = ARRAY_SIZE(argv);
|
|
CommandLineOptions options;
|
|
options.Parse(argc, argv, CmdLineMode::Application);
|
|
EXPECT_TRUE(options.fullscreen.value_or(false));
|
|
if (options.bootFilenames.empty()) {
|
|
EXPECT_TRUE(false);
|
|
return false;
|
|
}
|
|
EXPECT_EQ_STR(options.bootFilenames[0], std::string("My_Game.iso"));
|
|
EXPECT_TRUE(options.gpuBackend.has_value());
|
|
EXPECT_EQ_INT((int)options.gpuBackend.value_or((GPUBackend)-1), (int)GPUBackend::DIRECT3D11);
|
|
EXPECT_TRUE(options.pauseMenuExit.value_or(false));
|
|
}
|
|
// --timeout is headless-only (only headless/Headless.cpp reads it), so it must be parsed in Headless mode.
|
|
{
|
|
const char *argv[] = {
|
|
"ppsspp",
|
|
"--timeout=3",
|
|
"My_Game.iso"
|
|
};
|
|
int argc = ARRAY_SIZE(argv);
|
|
CommandLineOptions options;
|
|
options.Parse(argc, argv, CmdLineMode::Headless);
|
|
EXPECT_EQ_INT(options.timeout.value_or(0), 3);
|
|
}
|
|
// Test GL version override
|
|
{
|
|
const char *argv[] = {
|
|
"ppsspp",
|
|
"--graphics=gles3.3",
|
|
};
|
|
int argc = ARRAY_SIZE(argv);
|
|
CommandLineOptions options;
|
|
options.Parse(argc, argv);
|
|
EXPECT_EQ_INT(options.force_gl_version, 33);
|
|
}
|
|
return true;
|
|
}
|
|
|
|
// Check that RTTI is working.
|
|
bool TestLang() {
|
|
struct Base { virtual ~Base() = default; };
|
|
struct Derived : Base {};
|
|
|
|
Base* b = new Derived;
|
|
bool equals = typeid(*b) == typeid(Derived);
|
|
EXPECT_TRUE(equals);
|
|
return true;
|
|
}
|
|
|
|
typedef bool (*TestFunc)();
|
|
struct TestItem {
|
|
const char *name;
|
|
TestFunc func;
|
|
};
|
|
|
|
#define TEST_ITEM(name) { #name, &Test ##name, }
|
|
|
|
bool TestArmEmitter();
|
|
bool TestArm64Emitter();
|
|
bool TestX64Emitter();
|
|
bool TestRiscVEmitter();
|
|
bool TestLoongArch64Emitter();
|
|
bool TestShaderGenerators();
|
|
bool TestSoftwareGPUJit();
|
|
bool TestIRPassSimplify();
|
|
bool TestThreadManager();
|
|
bool TestVFS();
|
|
bool TestZipSlip();
|
|
bool TestLzrc();
|
|
bool TestTextureReplacer();
|
|
|
|
TestItem availableTests[] = {
|
|
#if PPSSPP_ARCH(ARM64) || PPSSPP_ARCH(AMD64) || PPSSPP_ARCH(X86)
|
|
TEST_ITEM(Arm64Emitter),
|
|
#endif
|
|
#if PPSSPP_ARCH(ARM) || PPSSPP_ARCH(AMD64) || PPSSPP_ARCH(X86)
|
|
TEST_ITEM(ArmEmitter),
|
|
#endif
|
|
#if PPSSPP_ARCH(AMD64) || PPSSPP_ARCH(X86)
|
|
TEST_ITEM(X64Emitter),
|
|
#endif
|
|
#if PPSSPP_ARCH(AMD64) || PPSSPP_ARCH(X86) || PPSSPP_ARCH(RISCV64)
|
|
TEST_ITEM(RiscVEmitter),
|
|
#endif
|
|
#if PPSSPP_ARCH(AMD64) || PPSSPP_ARCH(X86) || PPSSPP_ARCH(LOONGARCH64)
|
|
TEST_ITEM(LoongArch64Emitter),
|
|
#endif
|
|
TEST_ITEM(VertexJit),
|
|
TEST_ITEM(Asin),
|
|
TEST_ITEM(SinCos),
|
|
TEST_ITEM(VFPUSinCos),
|
|
TEST_ITEM(MathUtil),
|
|
TEST_ITEM(Parsers),
|
|
TEST_ITEM(TruncateCpy),
|
|
TEST_ITEM(MemBlockInfoSaveState),
|
|
TEST_ITEM(Serializer),
|
|
TEST_ITEM(BlockAllocator),
|
|
TEST_ITEM(SymbolMap),
|
|
TEST_ITEM(Hashmaps),
|
|
TEST_ITEM(Breakpoints),
|
|
TEST_ITEM(TempBreakpoints),
|
|
TEST_ITEM(Utf8),
|
|
TEST_ITEM(IRPassSimplify),
|
|
TEST_ITEM(Jit),
|
|
TEST_ITEM(VFPUMatrixTranspose),
|
|
TEST_ITEM(ParseLBN),
|
|
TEST_ITEM(QuickTexHash),
|
|
TEST_ITEM(CLZ),
|
|
TEST_ITEM(MemMap),
|
|
TEST_ITEM(ShaderGenerators),
|
|
TEST_ITEM(SoftwareGPUJit),
|
|
TEST_ITEM(Path),
|
|
TEST_ITEM(AndroidContentURI),
|
|
TEST_ITEM(ThreadManager),
|
|
TEST_ITEM(WrapText),
|
|
TEST_ITEM(TinySet),
|
|
TEST_ITEM(FastVec),
|
|
TEST_ITEM(SmallDataConvert),
|
|
TEST_ITEM(InputMapping),
|
|
TEST_ITEM(EscapeMenuString),
|
|
TEST_ITEM(VFS),
|
|
TEST_ITEM(Substitutions),
|
|
TEST_ITEM(IniFile),
|
|
TEST_ITEM(ColorConv),
|
|
TEST_ITEM(CharQueue),
|
|
TEST_ITEM(Buffer),
|
|
TEST_ITEM(SIMD),
|
|
TEST_ITEM(CrossSIMD),
|
|
TEST_ITEM(VolumeFunc),
|
|
TEST_ITEM(SplitSearch),
|
|
TEST_ITEM(FriendlyPath),
|
|
TEST_ITEM(LinAlg),
|
|
TEST_ITEM(Lang),
|
|
TEST_ITEM(CmdLine),
|
|
TEST_ITEM(ZipSlip),
|
|
TEST_ITEM(Lzrc),
|
|
TEST_ITEM(TextureReplacer),
|
|
};
|
|
|
|
int main(int argc, const char *argv[]) {
|
|
// Never block on a modal dialog - these get run from CI and from tooling.
|
|
SetupCRT(true);
|
|
SetCurrentThreadName("UnitTest");
|
|
TimeInit();
|
|
|
|
printf("CPU name: %s\n", cpu_info.cpu_string);
|
|
printf("ABI: %s\n", GetCompilerABI());
|
|
|
|
// In case we're on ARM, assume these are available.
|
|
cpu_info.bNEON = true;
|
|
cpu_info.bVFP = true;
|
|
cpu_info.bVFPv3 = true;
|
|
cpu_info.bVFPv4 = true;
|
|
g_Config.bEnableLogging = true;
|
|
g_logManager.DisableOutput(LogOutput::DebugString); // not really needed
|
|
|
|
// Collect the set of tests to run: "all", or one or more test names by
|
|
// (case-insensitive) name. Every non-"all" argument must match a known test name, or we
|
|
// bail out with the usage text - a silent partial run (e.g. from a typo) would be worse
|
|
// than an error.
|
|
std::vector<TestItem> testsToRun;
|
|
bool badArg = false;
|
|
if (argc == 2 && !strcasecmp(argv[1], "all")) {
|
|
for (const auto &f : availableTests) {
|
|
testsToRun.push_back(f);
|
|
}
|
|
} else {
|
|
for (int i = 1; i < argc; ++i) {
|
|
const TestItem *found = nullptr;
|
|
for (const auto &f : availableTests) {
|
|
if (!strcasecmp(argv[i], f.name)) {
|
|
found = &f;
|
|
break;
|
|
}
|
|
}
|
|
if (found) {
|
|
testsToRun.push_back(*found);
|
|
} else {
|
|
fprintf(stderr, "Unknown test: %s\n", argv[i]);
|
|
badArg = true;
|
|
}
|
|
}
|
|
}
|
|
|
|
if (testsToRun.empty() || badArg) {
|
|
fprintf(stderr, "You may select tests to run by passing one or more arguments, either \"all\" or one or more of the below.\n");
|
|
fprintf(stderr, "\n");
|
|
fprintf(stderr, "Available tests:\n");
|
|
for (auto f : availableTests) {
|
|
fprintf(stderr, " * %s\n", f.name);
|
|
}
|
|
return 1;
|
|
}
|
|
|
|
int passes = 0;
|
|
int fails = 0;
|
|
std::vector<const char *> failedTests;
|
|
for (const auto &f : testsToRun) {
|
|
printf("\n**** Running test %s ****\n", f.name);
|
|
if (f.func()) {
|
|
++passes;
|
|
} else {
|
|
printf("%s: FAILED\n", f.name);
|
|
failedTests.push_back(f.name);
|
|
++fails;
|
|
}
|
|
}
|
|
if (passes > 0) {
|
|
printf("%d tests passed.\n", passes);
|
|
}
|
|
if (fails > 0) {
|
|
printf("%d tests failed!\n", fails);
|
|
for (auto testName : failedTests) {
|
|
printf(" * %s\n", testName);
|
|
}
|
|
return 2;
|
|
}
|
|
|
|
return 0;
|
|
}
|