Progress with player

This commit is contained in:
Sergio Martin 2024-01-11 19:44:54 +01:00
parent d310134441
commit 16143afe0d
16 changed files with 293 additions and 123 deletions

2
.gitmodules vendored
View File

@ -1,3 +1,3 @@
[submodule "tests/roms/nes-test-roms"]
path = tests/nes-test-roms
path = tests/roms/test-roms
url = git@github.com:christopherpow/nes-test-roms.git

40
extern/hqn/hqn.cpp vendored
View File

@ -183,45 +183,5 @@ double HQNState::getFPS() const
return fps;
}
// Copied from bizinterface.cpp in BizHawk/quicknes
void HQNState::blit(int32_t *dest, const int32_t *colors, int cropleft, int croptop, int cropright, int cropbottom) const
{
// what is the point of the 256 color bitmap and the dynamic color allocation to it?
// why not just render directly to a 512 color bitmap with static palette positions?
// Nes_Emu *e = m_emu; // e was a parameter but since this is now part of a class, it's just in here
// const int srcpitch = e->frame().pitch;
// const unsigned char *src = e->frame().pixels;
// const unsigned char *const srcend = src + (e->image_height - cropbottom) * srcpitch;
//
// const short *lut = e->frame().palette;
//
// const int rowlen = 256 - cropleft - cropright;
//
// src += cropleft;
// src += croptop * srcpitch;
//
// for (; src < srcend; src += srcpitch)
// {
// for (int i = 0; i < rowlen; i++)
// {
// *dest++ = colors[lut[src[i]]];
// }
// }
Nes_Emu *e = m_emu; // e was a parameter but since this is now part of a class, it's just in here
const unsigned char *in_pixels = e->frame().pixels;
int32_t *out_pixels = dest;
for (unsigned h = 0; h < Nes_Emu::image_height; h++, in_pixels += e->frame().pitch, out_pixels += Nes_Emu::image_width)
for (unsigned w = 0; w < Nes_Emu::image_width; w++)
{
unsigned col = e->frame().palette[in_pixels[w]];
const Nes_Emu::rgb_t& rgb = m_emu->nes_colors[col];
unsigned r = rgb.red;
unsigned g = rgb.green;
unsigned b = rgb.blue;
out_pixels[w] = (r << 16) | (g << 8) | (b << 0);
}
}
} // end namespace hqn

47
extern/hqn/hqn.h vendored
View File

@ -3,6 +3,7 @@
#include <core/Nes_Emu.h>
#include <cstdint>
#include <stdio.h>
#define BLIT_SIZE 65536
@ -104,12 +105,6 @@ public:
*/
double getFPS() const;
/**
* Blit the contents of the NES screen to dest. Dest should be able to hold 256*240 ints.
* Unless you want to change the color palette you should use NES_VIDEO_PALETTE for colors.
*/
void blit(int32_t *dest, const int32_t *colors, int cropleft, int croptop, int cropright, int cropbottom) const;
inline HQNListener *getListener() const
{ return m_listener; }
@ -152,5 +147,45 @@ void printUsage(const char *filename);
} // end namespace hqn
// Copied from bizinterface.cpp in BizHawk/quicknes
inline void saveBlit(const Nes_Emu *e, int32_t *dest, const int32_t *colors, int cropleft, int croptop, int cropright, int cropbottom)
{
// what is the point of the 256 color bitmap and the dynamic color allocation to it?
// why not just render directly to a 512 color bitmap with static palette positions?
// Nes_Emu *e = m_emu; // e was a parameter but since this is now part of a class, it's just in here
// const int srcpitch = e->frame().pitch;
// const unsigned char *src = e->frame().pixels;
// const unsigned char *const srcend = src + (e->image_height - cropbottom) * srcpitch;
//
// const short *lut = e->frame().palette;
//
// const int rowlen = 256 - cropleft - cropright;
//
// src += cropleft;
// src += croptop * srcpitch;
//
// for (; src < srcend; src += srcpitch)
// {
// for (int i = 0; i < rowlen; i++)
// {
// *dest++ = colors[lut[src[i]]];
// }
// }
const unsigned char *in_pixels = e->frame().pixels;
if (in_pixels == NULL) return;
int32_t *out_pixels = dest;
for (unsigned h = 0; h < Nes_Emu::image_height; h++, in_pixels += e->frame().pitch, out_pixels += Nes_Emu::image_width)
for (unsigned w = 0; w < Nes_Emu::image_width; w++)
{
unsigned col = e->frame().palette[in_pixels[w]];
const Nes_Emu::rgb_t& rgb = e->nes_colors[col];
unsigned r = rgb.red;
unsigned g = rgb.green;
unsigned b = rgb.blue;
out_pixels[w] = (r << 16) | (g << 8) | (b << 0);
}
}
#endif /* __HQN_H__ */

View File

@ -190,7 +190,6 @@ void GUIController::update(bool readNES)
if (SDL_LockTexture(m_tex, nullptr, &nesPixels, &pitch) < 0) return;
m_state.blit((int32_t*)nesPixels, HQNState::NES_VIDEO_PALETTE, 0, 0, 0, 0);
SDL_UnlockTexture(m_tex);
// render to screen
@ -201,7 +200,7 @@ void GUIController::update(bool readNES)
processEvents();
}
void GUIController::update_blit(int32_t* blit, SDL_Surface* base, SDL_Surface* button_a, SDL_Surface* button_b, SDL_Surface* button_select, SDL_Surface* button_start, SDL_Surface* button_up, SDL_Surface* button_down, SDL_Surface* button_left, SDL_Surface* button_right)
void GUIController::update_blit(const int32_t* blit, SDL_Surface* base, SDL_Surface* button_a, SDL_Surface* button_b, SDL_Surface* button_select, SDL_Surface* button_start, SDL_Surface* button_up, SDL_Surface* button_down, SDL_Surface* button_left, SDL_Surface* button_right)
{
void *nesPixels = nullptr;
int pitch = 0;

View File

@ -99,7 +99,7 @@ public:
*/
void setCloseOperation(CloseOperation ops);
CloseOperation getCloseOperation() const;
void update_blit(int32_t* blit, SDL_Surface* base, SDL_Surface* button_a, SDL_Surface* button_b, SDL_Surface* button_select, SDL_Surface* button_start, SDL_Surface* button_up, SDL_Surface* button_down, SDL_Surface* button_left, SDL_Surface* button_right);
void update_blit(const int32_t* blit, SDL_Surface* base, SDL_Surface* button_a, SDL_Surface* button_b, SDL_Surface* button_select, SDL_Surface* button_start, SDL_Surface* button_up, SDL_Surface* button_down, SDL_Surface* button_left, SDL_Surface* button_right);
// Methods overriden from superclass.
virtual void onLoadROM(HQNState *state, const char *filename);

View File

@ -59,7 +59,7 @@ quickerNESPlayerSrc = [
quickerNESPlayerIncludeDirs = include_directories([ 'source'])
quickerNESPlayerDependencies = [ dependency('sdl2'), dependency('SDL2_image') ]
quickerNESPlayerCPPFlags = [ ]
quickerNESPlayerCPPFlags = [ '-DNCURSES' ]
quickerNESPlayerCFlags = [ ]
quickerNESPlayerLinkArgs = [ '-lncurses' ]

View File

@ -177,7 +177,10 @@ const char * Nes_Emu::emulate_frame( int joypad1, int joypad2 )
f->joypad_read_count = emu.joypad_read_count;
f->burst_phase = emu.ppu.burst_phase;
f->pitch = emu.ppu.host_row_bytes;
f->pixels = emu.ppu.host_pixels + f->left;
if (emu.ppu.host_pixels != NULL)
f->pixels = emu.ppu.host_pixels + f->left;
else
f->pixels = NULL;
}
else
{

View File

@ -46,6 +46,8 @@ public:
enum { image_width = 256 };
enum { image_height = 240 };
const uint8_t* getHostPixels () const { return emu.ppu.host_pixels; }
// Basic emulation
// Emulate one video frame using joypad1 and joypad2 as input. Afterwards, image
@ -265,6 +267,7 @@ inline void Nes_Emu::set_pixels( void* p, long n )
emu.ppu.host_row_bytes = n;
}
inline uint8_t const* Nes_Emu::chr_mem()
{
return cart()->chr_size() ? (uint8_t*) cart()->chr() : emu.ppu.impl->chr_ram;

View File

@ -32,11 +32,6 @@ class EmuInstance
auto result = _nes->load_ines(romFile);
if (result != 0) EXIT_WITH_ERROR("Could not initialize emulator with rom file: %s\n", romFilePath.c_str());
// Setting base memory pointers
_baseMem = _nes->low_mem();
_ppuNameTableMem = _nes->nametable_mem();
_highMem = _nes->high_mem();
// Getting state size to use
_stateSize = getStateSizeImpl();
@ -44,6 +39,10 @@ class EmuInstance
if (stateFilePath != "") loadStateFile(stateFilePath);
}
uint8_t* getLowMem() { return _nes->low_mem(); };
uint8_t* getNametableMem() { return _nes->nametable_mem(); };
uint8_t* getHighMem() { return _nes->high_mem();};
void loadStateFile(const std::string& stateFilePath)
{
// Loading state data
@ -167,11 +166,6 @@ class EmuInstance
// Emulator instance
Nes_Emu* _nes;
// Base low-memory pointer
uint8_t* _baseMem;
uint8_t* _ppuNameTableMem;
uint8_t* _highMem;
// State size for the given rom
size_t _stateSize;

View File

@ -11,39 +11,54 @@
#define _INVERSE_FRAME_RATE 16667
struct stepData_t
{
std::string input;
uint8_t* stateData;
int32_t curBlit[BLIT_SIZE];
};
class PlaybackInstance
{
private:
// Storage for the HQN state
hqn::HQNState _hqnState;
// Storage for the HQN GUI controller
hqn::GUIController* _hqnGUI;
// Pointer to the contained emulator instance
EmuInstance* const _emu;
// Flag to store whether to use the button overlay
bool _useOverlay = false;
// Overlay info
std::string _overlayPath;
SDL_Surface* _overlayBaseSurface = NULL;
SDL_Surface* _overlayButtonASurface;
SDL_Surface* _overlayButtonBSurface;
SDL_Surface* _overlayButtonSelectSurface;
SDL_Surface* _overlayButtonStartSurface;
SDL_Surface* _overlayButtonLeftSurface;
SDL_Surface* _overlayButtonRightSurface;
SDL_Surface* _overlayButtonUpSurface;
SDL_Surface* _overlayButtonDownSurface;
public:
void addStep(const std::string& input)
{
stepData_t step;
step.input = input;
step.stateData = (uint8_t*) calloc(_emu->getStateSize(), 1);
_emu->serializeState(step.stateData);
saveBlit(_emu->getInternalEmulator(), step.curBlit, hqn::HQNState::NES_VIDEO_PALETTE, 0, 0, 0, 0);
// Adding the step into the sequence
_stepSequence.push_back(step);
}
// Initializes the playback module instance
PlaybackInstance(EmuInstance* emu, const std::string& overlayPath = "") : _emu(emu)
PlaybackInstance(EmuInstance* emu, const std::string sequenceString, const std::string& overlayPath = "") : _emu(emu)
{
// Loading Emulator instance HQN
_hqnState.m_emu = _emu->getInternalEmulator();
static uint8_t video_buffer[Nes_Emu::image_width * Nes_Emu::image_height];
_hqnState.m_emu->set_pixels(video_buffer, Nes_Emu::image_width+8);
// Building sequence information
const auto inputSequence = split(sequenceString, ' ');
// Building sequence information
for (const auto& input : inputSequence)
{
// Adding new step
addStep(input);
// Advance state based on the input received
_emu->advanceState(input);
}
// Adding last step with no input
addStep("<End Of Sequence>");
// Loading overlay, if provided
if (overlayPath != "")
{
// Using overlay
@ -89,11 +104,6 @@ class PlaybackInstance
if (_overlayButtonDownSurface == NULL) EXIT_WITH_ERROR("[Error] Could not load image: %s, Reason: %s\n", imagePath.c_str(), SDL_GetError());
}
// Loading Emulator instance HQN
_hqnState.m_emu = _emu->getInternalEmulator();
static uint8_t video_buffer[Nes_Emu::image_width * Nes_Emu::image_height];
_hqnState.m_emu->set_pixels(video_buffer, Nes_Emu::image_width+8);
// Opening rendering window
SDL_SetMainReady();
@ -108,14 +118,13 @@ class PlaybackInstance
}
// Function to render frame
void renderFrame(const uint16_t currentStep, const std::string& move)
void renderFrame(const size_t stepId)
{
// Sleeping for the inverse frame rate
usleep(_INVERSE_FRAME_RATE);
// Storing current game state
uint8_t emuState[_emu->getStateSize()];
_emu->serializeState(emuState);
// Checking the required step id does not exceed contents of the sequence
if (stepId > _stepSequence.size()) EXIT_WITH_ERROR("[Error] Attempting to render a step larger than the step sequence");
// Getting step information
const auto& step = _stepSequence[stepId];
// Pointer to overlay images (NULL if unused)
SDL_Surface* overlayButtonASurface = NULL;
@ -130,24 +139,76 @@ class PlaybackInstance
// Load correct overlay images, if using overlay
if (_useOverlay == true)
{
if (move.find("A") != std::string::npos) overlayButtonASurface = _overlayButtonASurface;
if (move.find("B") != std::string::npos) overlayButtonBSurface = _overlayButtonBSurface;
if (move.find("S") != std::string::npos) overlayButtonSelectSurface = _overlayButtonSelectSurface;
if (move.find("T") != std::string::npos) overlayButtonStartSurface = _overlayButtonStartSurface;
if (move.find("L") != std::string::npos) overlayButtonLeftSurface = _overlayButtonLeftSurface;
if (move.find("R") != std::string::npos) overlayButtonRightSurface = _overlayButtonRightSurface;
if (move.find("U") != std::string::npos) overlayButtonUpSurface = _overlayButtonUpSurface;
if (move.find("D") != std::string::npos) overlayButtonDownSurface = _overlayButtonDownSurface;
if (step.input.find("A") != std::string::npos) overlayButtonASurface = _overlayButtonASurface;
if (step.input.find("B") != std::string::npos) overlayButtonBSurface = _overlayButtonBSurface;
if (step.input.find("S") != std::string::npos) overlayButtonSelectSurface = _overlayButtonSelectSurface;
if (step.input.find("T") != std::string::npos) overlayButtonStartSurface = _overlayButtonStartSurface;
if (step.input.find("L") != std::string::npos) overlayButtonLeftSurface = _overlayButtonLeftSurface;
if (step.input.find("R") != std::string::npos) overlayButtonRightSurface = _overlayButtonRightSurface;
if (step.input.find("U") != std::string::npos) overlayButtonUpSurface = _overlayButtonUpSurface;
if (step.input.find("D") != std::string::npos) overlayButtonDownSurface = _overlayButtonDownSurface;
}
// Since renderer is off by one frame, we need to emulate an additional frame
_emu->advanceState(0,0);
int32_t curImage[BLIT_SIZE];
_hqnState.blit(curImage, hqn::HQNState::NES_VIDEO_PALETTE, 0, 0, 0, 0);
_hqnGUI->update_blit(curImage, _overlayBaseSurface, overlayButtonASurface, overlayButtonBSurface, overlayButtonSelectSurface, overlayButtonStartSurface, overlayButtonLeftSurface, overlayButtonRightSurface, overlayButtonUpSurface, overlayButtonDownSurface);
// Reload game state
_emu->deserializeState(emuState);
// Updating image
_hqnGUI->update_blit(step.curBlit, _overlayBaseSurface, overlayButtonASurface, overlayButtonBSurface, overlayButtonSelectSurface, overlayButtonStartSurface, overlayButtonLeftSurface, overlayButtonRightSurface, overlayButtonUpSurface, overlayButtonDownSurface);
}
size_t getSequenceLength() const
{
return _stepSequence.size();
}
const std::string getInput(const size_t stepId) const
{
// Checking the required step id does not exceed contents of the sequence
if (stepId > _stepSequence.size()) EXIT_WITH_ERROR("[Error] Attempting to render a step larger than the step sequence");
// Getting step information
const auto& step = _stepSequence[stepId];
// Returning step input
return step.input;
}
const uint8_t* getStateData(const size_t stepId) const
{
// Checking the required step id does not exceed contents of the sequence
if (stepId > _stepSequence.size()) EXIT_WITH_ERROR("[Error] Attempting to render a step larger than the step sequence");
// Getting step information
const auto& step = _stepSequence[stepId];
// Returning step input
return step.stateData;
}
private:
// Internal sequence information
std::vector<stepData_t> _stepSequence;
// Storage for the HQN state
hqn::HQNState _hqnState;
// Storage for the HQN GUI controller
hqn::GUIController* _hqnGUI;
// Pointer to the contained emulator instance
EmuInstance* const _emu;
// Flag to store whether to use the button overlay
bool _useOverlay = false;
// Overlay info
std::string _overlayPath;
SDL_Surface* _overlayBaseSurface = NULL;
SDL_Surface* _overlayButtonASurface;
SDL_Surface* _overlayButtonBSurface;
SDL_Surface* _overlayButtonSelectSurface;
SDL_Surface* _overlayButtonStartSurface;
SDL_Surface* _overlayButtonLeftSurface;
SDL_Surface* _overlayButtonRightSurface;
SDL_Surface* _overlayButtonUpSurface;
SDL_Surface* _overlayButtonDownSurface;
};

View File

@ -50,17 +50,105 @@ int main(int argc, char *argv[])
// Getting reproduce flag
bool disableRender = program.get<bool>("--disableRender");
// Loading sequence file
std::string inputSequence;
auto status = loadStringFromFile(inputSequence, sequenceFilePath.c_str());
if (status == false) EXIT_WITH_ERROR("[ERROR] Could not find or read from sequence file: %s\n", sequenceFilePath.c_str());
// Initializing terminal
initializeTerminal();
// Printing provided parameters
printf("Rom File Path: %s\n", romFilePath.c_str());
printf("Sequence File Path: %s\n", sequenceFilePath.c_str());
printf("State File Path: %s\n", stateFilePath.c_str());
printw("[] Rom File Path: '%s'\n", romFilePath.c_str());
printw("[] Sequence File Path: '%s'\n", sequenceFilePath.c_str());
printw("[] State File Path: '%s'\n", stateFilePath.empty() ? "<Boot Start>" : stateFilePath.c_str());
printw("[] Generating Sequence...\n");
refreshTerminal();
// Creating emulator instance
auto e = EmuInstance(romFilePath, stateFilePath);
// Creating playback instance
auto p = PlaybackInstance(&e);
auto p = PlaybackInstance(&e, inputSequence);
while(true) p.renderFrame(0, "");
// Flag to continue running playback
bool continueRunning = true;
// Variable for current step in view
ssize_t sequenceLength = p.getSequenceLength();
ssize_t currentStep = 0;
// Flag to display frame information
bool showFrameInfo = true;
// Interactive section
while(continueRunning)
{
// Updating display
if (disableRender == false) p.renderFrame(currentStep);
// Getting input
const auto& input = p.getInput(currentStep);
// Getting state data
//const auto stateData = p.getStateData(currentStep);
// Printing data and commands
if (showFrameInfo)
{
clearTerminal();
printw("[] ----------------------------------------------------------------\n");
printw("[] Current Step #: %lu / %lu\n", currentStep + 1, sequenceLength);
printw("[] Input: %s\n", input.c_str());
// Only print commands if not in reproduce mode
if (isReproduce == false) printw("[] Commands: n: -1 m: +1 | h: -10 | j: +10 | y: -100 | u: +100 | k: -1000 | i: +1000 | s: quicksave | p: play | q: quit\n");
refreshTerminal();
}
// Resetting show frame info flag
showFrameInfo = true;
// Get command
auto command = getKeyPress();
// Advance/Rewind commands
if (command == 'n') currentStep = currentStep - 1;
if (command == 'm') currentStep = currentStep + 1;
if (command == 'h') currentStep = currentStep - 10;
if (command == 'j') currentStep = currentStep + 10;
if (command == 'y') currentStep = currentStep - 100;
if (command == 'u') currentStep = currentStep + 100;
if (command == 'k') currentStep = currentStep - 1000;
if (command == 'i') currentStep = currentStep + 1000;
// Correct current step if requested more than possible
if (currentStep < 0) currentStep = 0;
if (currentStep >= sequenceLength) currentStep = sequenceLength-1;
// Quicksave creation command
if (command == 's')
{
// Storing state file
std::string saveFileName = "quicksave.state";
e.saveStateFile(saveFileName);
printw("[] Saved state to %s\n", saveFileName.c_str());
// Do no show frame info again after this action
showFrameInfo = false;
}
// Start playback from current point
if (command == 'p') isReproduce = true;
// Start playback from current point
if (command == 'q') continueRunning = false;
}
// Ending ncurses window
finalizeTerminal();
}

View File

@ -7,6 +7,7 @@
#include <stdarg.h>
#include <stdexcept>
#include <stdio.h>
#include <unistd.h>
#include <algorithm>
// If we use NCurses, we need to use the appropriate printing function
@ -56,7 +57,33 @@ inline int getKeyPress()
return getch();
}
#endif
void initializeTerminal()
{
// Initializing ncurses screen
initscr();
cbreak();
noecho();
nodelay(stdscr, TRUE);
scrollok(stdscr, TRUE);
}
void clearTerminal()
{
clear();
}
void finalizeTerminal()
{
endwin();
}
void refreshTerminal()
{
refresh();
}
#endif // NCURSES
// Function to split a string into a sub-strings delimited by a character
// Taken from stack overflow answer to https://stackoverflow.com/questions/236129/how-do-i-iterate-over-the-words-of-a-string