mirror of
https://github.com/Awakenchan/LanternEngine.git
synced 2026-11-10 09:21:42 -10:00
199 lines
9.3 KiB
C++
199 lines
9.3 KiB
C++
// SPDX-License-Identifier: GPL-3.0-or-later
|
|
#include "gpu/shim/engine/frame_rate.cpp"
|
|
#include <cstdarg>
|
|
#include <cstdio>
|
|
#include <cstdlib>
|
|
#ifdef _WIN32
|
|
#include <malloc.h>
|
|
#endif
|
|
|
|
namespace BbEngine {
|
|
void host_log(const char* format, ...) {
|
|
va_list args;
|
|
va_start(args, format);
|
|
std::vprintf(format, args);
|
|
va_end(args);
|
|
std::putchar('\n');
|
|
}
|
|
}
|
|
|
|
using namespace BbEngine::FrameRate;
|
|
|
|
namespace {
|
|
int failures = 0;
|
|
void Check(bool ok, const char* what) {
|
|
if (!ok) { std::printf("FAIL: %s\n", what); ++failures; }
|
|
}
|
|
|
|
float FloatAt(const std::uint8_t* image, std::uint32_t offset) {
|
|
float f;
|
|
std::memcpy(&f, image + offset, 4);
|
|
return f;
|
|
}
|
|
std::int32_t IntAt(const std::uint8_t* image, std::uint32_t offset) {
|
|
std::int32_t i;
|
|
std::memcpy(&i, image + offset, 4);
|
|
return i;
|
|
}
|
|
|
|
void SetFloat(std::uint8_t* image, std::uint32_t offset, float f) { std::memcpy(image + offset, &f, 4); }
|
|
|
|
std::uint8_t* Image(std::size_t size) {
|
|
#ifdef _WIN32
|
|
auto* image = static_cast<std::uint8_t*>(_aligned_malloc(size, 0x1000));
|
|
#else
|
|
auto* image = static_cast<std::uint8_t*>(std::aligned_alloc(0x1000, size));
|
|
#endif
|
|
std::memset(image, 0, size);
|
|
std::memcpy(image + kFrameTimeCave, kFrameTimeCaveCode, sizeof kFrameTimeCaveCode);
|
|
for (const Site& s : kSites) {
|
|
if (s.checked) std::memcpy(image + s.offset - 4, s.prefix, 4);
|
|
const std::uint32_t bits = Value(s.kind, 60.0f);
|
|
std::memcpy(image + s.offset, &bits, 4);
|
|
}
|
|
SetFloat(image, kSlopeSites[0].offset, 1.0f);
|
|
SetFloat(image, kSlopeSites[1].offset, 0.65f);
|
|
return image;
|
|
}
|
|
|
|
const Site* First(SiteKind kind, bool code) {
|
|
for (const Site& s : kSites)
|
|
if (&s != &kSites[0] && s.kind == kind && s.code == code) return &s;
|
|
return nullptr;
|
|
}
|
|
}
|
|
|
|
int main() {
|
|
const std::size_t size = 0x5400000;
|
|
Check(std::fabs(BbEngine::FrameRate::HeldRate()) < 1e-6f, "nothing held before install");
|
|
std::uint8_t* blank = static_cast<std::uint8_t*>(std::calloc(size, 1));
|
|
Check(!BbEngine::FrameRate::Install(blank, size), "no frame-rate list: not installed");
|
|
std::free(blank);
|
|
|
|
std::uint8_t* image = Image(size);
|
|
const Site* code_step = First(SiteKind::Step, true);
|
|
const Site* data_step = First(SiteKind::Step, false);
|
|
const Site* sixth = First(SiteKind::CountSixth, true);
|
|
const Site* ease = First(SiteKind::EaseDistance, false);
|
|
Check(code_step && data_step && sixth && ease, "the table has code steps, data steps, a count and an ease");
|
|
std::memset(image + kSites[0].offset - 4, 0xcc, kSites[0].checked ? 4 : 0);
|
|
|
|
#ifdef _WIN32
|
|
_putenv_s("BB_FPS", "120");
|
|
_putenv_s("LANTERN_FRAMERATE_SLOPE_TICK", "1");
|
|
#else
|
|
setenv("BB_FPS", "120", 1);
|
|
setenv("LANTERN_FRAMERATE_SLOPE_TICK", "1", 1);
|
|
#endif
|
|
Check(BbEngine::FrameRate::Install(image, size), "installed over a 60 FPS list");
|
|
Check(std::fabs(FloatAt(image, code_step->offset) - 1.0f / 120) < 1e-7f, "code step at 1/120");
|
|
Check(std::fabs(FloatAt(image, data_step->offset) - 1.0f / 120) < 1e-7f, "data step at 1/120");
|
|
Check(IntAt(image, sixth->offset) == 20, "a sixth of a second is 20 frames at 120");
|
|
Check(std::fabs(FloatAt(image, ease->offset) - float(1.0 - std::pow(0.9, 0.25))) < 1e-6f, "distance ease for 120");
|
|
if (kSites[0].checked)
|
|
Check(std::fabs(FloatAt(image, kSites[0].offset) - 1.0f / 60) < 1e-7f, "a site whose code differs is left");
|
|
Check(std::fabs(FloatAt(image, kSlopeSites[0].offset) - 0.25f) < 1e-6f, "steep-ground slide gain a quarter until the first tick");
|
|
Check(std::fabs(FloatAt(image, kSlopeSites[1].offset) - float(std::pow(0.65, 0.25))) < 1e-6f,
|
|
"steep-ground slide decay 0.65^(1/4) at 120");
|
|
|
|
SetFloat(image, kFrameTime, 1.0f / 120);
|
|
int gains = 0, zeros = 0;
|
|
for (int i = 0; i < 120; ++i) {
|
|
BbEngine::FrameRate::Tick();
|
|
const float gain = FloatAt(image, kSlopeSites[0].offset);
|
|
gains += gain == 1.0f;
|
|
zeros += gain == 0.0f;
|
|
}
|
|
Check(gains == 30 && zeros == 90, "at 120 the slide builds the game's 1.0 on 30 frames a second, 0 between");
|
|
|
|
SetFloat(image, kFrameTime, 1.0f / 60);
|
|
BbEngine::FrameRate::Tick();
|
|
Check(std::fabs(FloatAt(image, data_step->offset) - 1.0f / 60) < 1e-7f, "data step follows the frame time at once");
|
|
gains = 0;
|
|
for (int i = 0; i < 60; ++i) {
|
|
BbEngine::FrameRate::Tick();
|
|
gains += FloatAt(image, kSlopeSites[0].offset) == 1.0f;
|
|
}
|
|
Check(gains == 30, "at 60 the slide builds on every second frame");
|
|
Check(std::fabs(FloatAt(image, kSlopeSites[1].offset) - float(std::pow(0.65, 0.5))) < 1e-6f, "slide decay follows it");
|
|
Check(std::fabs(FloatAt(image, code_step->offset) - 1.0f / 120) < 1e-7f, "code step waits for a held rate");
|
|
for (int i = 0; i < 400; ++i) BbEngine::FrameRate::Tick();
|
|
Check(std::fabs(FloatAt(image, code_step->offset) - 1.0f / 60) < 1e-6f, "code step follows a held 60");
|
|
Check(IntAt(image, sixth->offset) == 10, "the count follows a held 60");
|
|
Check(std::fabs(BbEngine::FrameRate::HeldRate() - 60.0f) < 1.0f, "held rate is 60");
|
|
|
|
SetFloat(image, kFrameTime, 1.0f / 20);
|
|
for (int i = 0; i < 400; ++i) BbEngine::FrameRate::Tick();
|
|
Check(std::fabs(FloatAt(image, data_step->offset) - 1.0f / 30) < 1e-7f, "no step longer than 1/30");
|
|
Check(FloatAt(image, kSlopeSites[0].offset) == 1.0f && FloatAt(image, kSlopeSites[1].offset) == 0.65f,
|
|
"at 30 the slide is the game's own 1.0 and 0.65");
|
|
|
|
SetFloat(image, kFrameTime, 1.0f / 240);
|
|
for (int i = 0; i < 400; ++i) BbEngine::FrameRate::Tick();
|
|
Check(std::fabs(FloatAt(image, code_step->offset) - 1.0f / 120) < 1e-6f, "never past the target");
|
|
|
|
std::uint8_t* fresh = static_cast<std::uint8_t*>(std::calloc(size, 1));
|
|
for (const ListWrite& w : kListWrites)
|
|
if (w.list == Ninety || w.list == Sprint) std::memcpy(fresh + w.offset, kListBlob + w.at, w.size);
|
|
g_target = 120.0f;
|
|
g_uncapped = false;
|
|
ApplyLists(fresh, size);
|
|
bool all = true;
|
|
for (const ListWrite& w : kListWrites)
|
|
if ((w.list == Ninety || w.list == Sprint) && std::memcmp(fresh + w.offset, kListBlob + w.at + w.size, w.size) != 0)
|
|
all = false;
|
|
Check(all, "the engine writes the 90 FPS code and the sprint fix over the game's own bytes");
|
|
Check(std::memcmp(fresh + kFrameTimeCave, kFrameTimeCaveCode, sizeof kFrameTimeCaveCode) == 0,
|
|
"the frame-time code is in place after the engine's writes");
|
|
std::free(fresh);
|
|
|
|
std::uint8_t* sixty = static_cast<std::uint8_t*>(std::calloc(size, 1));
|
|
int wall_writes = 0;
|
|
for (const ListWrite& w : kListWrites)
|
|
if (w.list == Sprint) { std::memcpy(sixty + w.offset, kListBlob + w.at, w.size); ++wall_writes; }
|
|
g_target = 60.0f;
|
|
Check(wall_writes > 0 && ApplyWallSpeed(sixty, size) == wall_writes, "the wall check is written at 60 FPS");
|
|
bool wall = true;
|
|
for (const ListWrite& w : kListWrites)
|
|
if (w.list == Sprint && std::memcmp(sixty + w.offset, kListBlob + w.at + w.size, w.size) != 0) wall = false;
|
|
Check(wall, "the wall check divides by the frame time at 60 FPS");
|
|
Check(ApplyWallSpeed(sixty, size) == 0, "the wall check is written once");
|
|
std::free(sixty);
|
|
|
|
std::uint8_t* world = static_cast<std::uint8_t*>(std::calloc(size, 1));
|
|
const std::uint8_t* game_room = GameBytes(kWorldCave, kWorldCaveSize);
|
|
Check(game_room != nullptr, "the game's bytes of the world step's spare room are known");
|
|
if (game_room) {
|
|
std::memcpy(world + kWorldCave, game_room, kWorldCaveSize);
|
|
std::memcpy(world + kWorldStep, kWorldStepCode, sizeof kWorldStepCode);
|
|
SetFloat(world, kWorldStep + 3, 1.0f / 120);
|
|
std::memcpy(world + kWorldLockFail, kWorldLockFailCode, sizeof kWorldLockFailCode);
|
|
Check(!ApplyWorldStep(world, size), "no world step without the game's longest step");
|
|
SetFloat(world, kLongestStep, 1.0f / 30);
|
|
Check(ApplyWorldStep(world, size), "the world step is written over the game's code");
|
|
auto rel = [&](std::size_t at) {
|
|
std::int32_t r;
|
|
std::memcpy(&r, world + at, 4);
|
|
return std::size_t(std::int64_t(at) + 4 + r);
|
|
};
|
|
Check(world[kWorldStep] == 0xe9 && rel(kWorldStep + 1) == kWorldCave, "the world step jumps to its room");
|
|
Check(rel(kWorldCave + 3) == kFlipper, "the room reads the flipper");
|
|
Check(world[kWorldCave + 7] == 0xc5 && world[kWorldCave + 11] == 0x64 && world[kWorldCave + 12] == 0x02,
|
|
"the room loads the measured frame time");
|
|
Check(world[kWorldCave + 19] == 0x18, "the room keeps the frame's shortest step");
|
|
Check(rel(kWorldCave + 24) == kLongestStep, "the room keeps the game's longest step");
|
|
Check(world[kWorldCave + 28] == 0xc5 && world[kWorldCave + 32] == 0xb8, "the room stores the world's step");
|
|
Check(world[kWorldCave + 33] == 0xe9 && rel(kWorldCave + 34) == kWorldStepNext, "the room returns after the step");
|
|
Check(std::memcmp(world + kWorldLockFail, kNop6, sizeof kNop6) == 0, "the lock failure no longer reaches the room");
|
|
Check(ApplyWorldStep(world, size), "a written world step is accepted again");
|
|
world[kWorldCave + 40] = 0x90;
|
|
std::memcpy(world + kWorldStep, kWorldStepCode, sizeof kWorldStepCode);
|
|
Check(!ApplyWorldStep(world, size), "a world step room that is not the game's is left");
|
|
}
|
|
std::free(world);
|
|
|
|
if (failures) return 1;
|
|
std::puts("frame rate: ok");
|
|
return 0;
|
|
}
|