Files
psxrecomp/runtime/tests/test_gpu_fmv_hd.cpp
T
2026-10-26 16:05:22 -07:00

131 lines
6.1 KiB
C++

#include "gpu_fmv_hd.h"
#include "psx_sdl.h"
#define STB_IMAGE_IMPLEMENTATION
#include "../third_party/stb_image.h"
#include "../src/png_write.h"
#include <atomic>
#include <chrono>
#include <cstdlib>
#include <cstring>
#include <filesystem>
#include <fstream>
#include <iostream>
static std::atomic<int> allocation_failure{-1};
static int thread_failure=-1, threads_started;
static unsigned failures;
extern "C" void *fmv_hd_test_malloc(size_t n) {
return allocation_failure.fetch_sub(1)==0?nullptr:std::malloc(n);
}
extern "C" void *fmv_hd_test_realloc(void *p,size_t n) {
return allocation_failure.fetch_sub(1)==0?nullptr:std::realloc(p,n);
}
extern "C" SDL_Thread *fmv_hd_test_start_thread(SDL_ThreadFunction fn,void *arg) {
if(threads_started++==thread_failure)return nullptr;
return SDL_CreateThread(fn,"fmv-test-decoder",arg);
}
static void check(bool ok,const char *why) {
if(!ok){std::cerr<<why<<'\n';++failures;}
}
int main() {
namespace fs=std::filesystem;
const auto root=fs::temp_directory_path()/fs::path("psx-fmv-safety-"+
std::to_string(std::chrono::steady_clock::now().time_since_epoch().count()));
fs::create_directories(root/"movie/frames");
#ifdef _WIN32
_putenv_s("PSX_FMV_HD_DIR",root.string().c_str());
#else
setenv("PSX_FMV_HD_DIR",root.string().c_str(),1);
#endif
std::ofstream(root/"liste.txt")<<"movie\n";
FmvHdIndexHeader header{};
std::memcpy(header.magic,"FMVHD003",8);
header.frames=header.sig_w=header.sig_h=header.sub=header.fps_den=1;
header.fps_num=30;header.src_w=header.src_h=header.disp_w=header.disp_h=2;
auto index=[&](FmvHdIndexHeader h) {
std::ofstream out(root/"movie/index.bin",std::ios::binary);
out.write(reinterpret_cast<const char*>(&h),sizeof h);out.put(0);
if(h.frames==2)out.put(0); /* deliberately identical source signatures */
const int32_t lba=0;out.write(reinterpret_cast<const char*>(&lba),sizeof lba);
if(h.frames==2){const int32_t next=10;out.write(reinterpret_cast<const char*>(&next),sizeof next);}
};
const uint8_t rgba[16]={0,0,0,255,0,0,0,255,0,0,0,255,0,0,0,255};
FILE *image=std::fopen((root/"movie/frames/000000.png").string().c_str(),"wb");
check(image && png_write_rgba(image,rgba,2,2),"create valid tiny movie frame");
if(image)std::fclose(image);
uint32_t original[4]={0xff000000u,0xff000000u,0xff000000u,0xff000000u};
FmvHdImage output{};
FmvHdTimeline timeline{1000, 1, 1, 1, 0, 0, 1};
auto lookup=[&]{return gpu_fmv_hd_lookup(original,2,2,2,timeline,&output);};
for(unsigned variant=0;variant<5;++variant) {
auto bad=header;
if(variant==0)bad.sig_w=UINT32_MAX;
if(variant==1)bad.frames=UINT32_MAX;
if(variant==2)bad.sub=UINT32_MAX;
if(variant==3)bad.off_x=INT32_MAX;
if(variant==4)bad.frames=3; // truncated signature payload
index(bad);check(!lookup(),"malformed index falls back to original");
gpu_fmv_hd_shutdown();
}
index(header);
for(int fail=0;fail<6;++fail) {
allocation_failure=fail;threads_started=0;
(void)lookup();gpu_fmv_hd_shutdown();
}
allocation_failure=-1;
for(int fail=0;fail<6;++fail) {
thread_failure=fail;threads_started=0;
check(!lookup(),"partial thread startup falls back and joins workers");
gpu_fmv_hd_shutdown();
}
thread_failure=-1;
for(int restart=0;restart<3;++restart) {
threads_started=0;bool found=false;
for(int i=0;i<500 && !found;++i){found=lookup()!=0;if(!found)SDL_Delay(2);}
check(found && output.w==2 && output.h==2 && output.rgba[3]==255,
"valid frame decodes after failure or session restart");
gpu_fmv_hd_shutdown();gpu_fmv_hd_shutdown();
}
/* Host sleep must not advance the movie. Only captured guest cycles may
* select the second HD subframe; loading an earlier timeline restarts it. */
auto interpolated=header; interpolated.sub=2; index(interpolated);
const uint8_t second[16]={255,0,0,255,255,0,0,255,255,0,0,255,255,0,0,255};
image=std::fopen((root/"movie/frames/000001.png").string().c_str(),"wb");
check(image && png_write_rgba(image,second,2,2),"create second movie subframe");
if(image)std::fclose(image);
timeline={1000,1,2,1,0,0,1};
bool first=false;
for(int i=0;i<200 && !first;++i){first=lookup() && output.rgba[0]==0;if(!first)SDL_Delay(2);}
check(first,"first subframe decoded");
SDL_Delay(50);
check(lookup() && output.rgba[0]==0,"pause holds guest subframe despite host time");
timeline.cycle+=600000; timeline.frame++;
bool advanced=false;
for(int i=0;i<200 && !advanced;++i){advanced=lookup() && output.rgba[0]==255;if(!advanced)SDL_Delay(2);}
check(advanced,"guest cycles advance interpolated subframe");
timeline.decode_serial++;
check(lookup() && output.rgba[0]==0,
"new guest MDEC decode restarts phase on identical source image");
timeline.cycle+=600000;
check(lookup() && output.rgba[0]==255,"phase advances again within same MDEC decode");
timeline={1000,1,3,1,0,0,1};
check(lookup() && output.rgba[0]==0,"state-load epoch restarts subframe");
original[0]=0xffffffffu;
check(!lookup(),"unrecognised movie scanout falls back to native image");
gpu_fmv_hd_shutdown();
original[0]=0xff000000u;
auto two=header;two.frames=2;two.stream_lba_hi=19;index(two);
timeline={1000,1,4,2,0,0,2};
bool mapped_first=false;
for(int i=0;i<200 && !mapped_first;++i){mapped_first=lookup() && output.rgba[0]==0;if(!mapped_first)SDL_Delay(2);}
check(mapped_first,"first identical source image uses first sector interval");
timeline.delivered_lba=10;timeline.decode_serial++;timeline.cycle+=1000;timeline.frame++;
bool mapped_second=false;
for(int i=0;i<200 && !mapped_second;++i){mapped_second=lookup() && output.rgba[0]==255;if(!mapped_second)SDL_Delay(2);}
check(mapped_second,"identical source image selects second HD frame by delivered sector");
gpu_fmv_hd_shutdown();
fs::remove_all(root);
if(!failures)std::cout<<"PASS FMV bounds, lifecycle and guest timeline\n";
return failures?1:0;
}