NES: Added HD pack builder tool

This commit is contained in:
Sour 2022-12-29 18:28:33 +09:00
parent db2db7ddb1
commit e96d6f3ca5
34 changed files with 1206 additions and 55 deletions

View file

@ -24,6 +24,8 @@
<ClInclude Include="Debugger\DisassemblySearch.h" />
<ClInclude Include="Debugger\FrozenAddressManager.h" />
<ClInclude Include="Debugger\StepBackManager.h" />
<ClInclude Include="NES\HdPacks\HdBuilderPpu.h" />
<ClInclude Include="NES\HdPacks\HdPackBuilder.h" />
<ClInclude Include="NES\Mappers\Audio\emu2413.h" />
<ClInclude Include="SNES\DSP\DspInterpolation.h" />
<ClInclude Include="SNES\DSP\Dsp.h" />
@ -702,6 +704,7 @@
<ClCompile Include="NES\HdPacks\HdAudioDevice.cpp" />
<ClCompile Include="NES\HdPacks\HdNesPack.cpp" />
<ClCompile Include="NES\HdPacks\HdNesPpu.cpp" />
<ClCompile Include="NES\HdPacks\HdPackBuilder.cpp" />
<ClCompile Include="NES\HdPacks\HdPackLoader.cpp" />
<ClCompile Include="NES\HdPacks\HdVideoFilter.cpp" />
<ClCompile Include="NES\HdPacks\OggMixer.cpp" />

View file

@ -2610,6 +2610,12 @@
<ClInclude Include="NES\Mappers\Audio\emu2413.h">
<Filter>NES\Mappers\Audio</Filter>
</ClInclude>
<ClInclude Include="NES\HdPacks\HdBuilderPpu.h">
<Filter>NES\HdPacks</Filter>
</ClInclude>
<ClInclude Include="NES\HdPacks\HdPackBuilder.h">
<Filter>NES\HdPacks</Filter>
</ClInclude>
</ItemGroup>
<ItemGroup>
<ClCompile Include="Shared\Video\RotateFilter.cpp">
@ -2741,6 +2747,9 @@
<ClCompile Include="NES\Mappers\Audio\emu2413.cpp">
<Filter>NES\Mappers\Audio</Filter>
</ClCompile>
<ClCompile Include="NES\HdPacks\HdPackBuilder.cpp">
<Filter>NES\HdPacks</Filter>
</ClCompile>
</ItemGroup>
<ItemGroup>
<Filter Include="PCE">

View file

@ -45,11 +45,10 @@ NesDebugger::NesDebugger(Debugger* debugger) : IDebugger(debugger->GetEmulator()
_console = console;
_cpu = console->GetCpu();
_ppu = console->GetPpu();
_mapper = console->GetMapper();
_memoryManager = console->GetMemoryManager();
_traceLogger.reset(new NesTraceLogger(debugger, this, _ppu));
_traceLogger.reset(new NesTraceLogger(debugger, this, console));
_disassembler = debugger->GetDisassembler();
_memoryAccessCounter = debugger->GetMemoryAccessCounter();
_settings = debugger->GetEmulator()->GetSettings();
@ -271,7 +270,7 @@ void NesDebugger::Step(int32_t stepCount, StepType type)
case StepType::CpuCycleStep: step.CpuCycleStepCount = stepCount; break;
case StepType::PpuStep: step.PpuStepCount = stepCount; break;
case StepType::PpuScanline: step.PpuStepCount = 341 * stepCount; break;
case StepType::PpuFrame: step.PpuStepCount = 341 * _ppu->GetScanlineCount() * stepCount; break;
case StepType::PpuFrame: step.PpuStepCount = 341 * _console->GetPpu()->GetScanlineCount() * stepCount; break;
case StepType::SpecificScanline: step.BreakScanline = stepCount; break;
}
_step.reset(new StepRequest(step));
@ -279,7 +278,7 @@ void NesDebugger::Step(int32_t stepCount, StepType type)
void NesDebugger::DrawPartialFrame()
{
_ppu->DebugSendFrame();
_console->GetPpu()->DebugSendFrame();
}
void NesDebugger::ProcessCallStackUpdates(AddressInfo& destAddr, uint16_t destPc)
@ -349,11 +348,11 @@ void NesDebugger::ProcessPpuWrite(uint16_t addr, uint8_t value, MemoryType memor
void NesDebugger::ProcessPpuCycle()
{
if(_ppuTools->HasOpenedViewer()) {
_ppuTools->UpdateViewers(_ppu->GetCurrentScanline(), _ppu->GetCurrentCycle());
_ppuTools->UpdateViewers(_console->GetPpu()->GetCurrentScanline(), _console->GetPpu()->GetCurrentCycle());
}
if(_step->HasRequest) {
if(_step->HasScanlineBreakRequest() && _ppu->GetCurrentCycle() == 0 && _ppu->GetCurrentScanline() == _step->BreakScanline) {
if(_step->HasScanlineBreakRequest() && _console->GetPpu()->GetCurrentCycle() == 0 && _console->GetPpu()->GetCurrentScanline() == _step->BreakScanline) {
_debugger->SleepUntilResume(CpuType::Nes, _step->GetBreakSource());
} else if(_step->PpuStepCount > 0) {
_step->PpuStepCount--;
@ -446,12 +445,12 @@ BaseState& NesDebugger::GetState()
void NesDebugger::GetPpuState(BaseState& state)
{
_ppu->GetState((NesPpuState&)state);
_console->GetPpu()->GetState((NesPpuState&)state);
}
void NesDebugger::SetPpuState(BaseState& state)
{
_ppu->SetState((NesPpuState&)state);
_console->GetPpu()->SetState((NesPpuState&)state);
}
ITraceLogger* NesDebugger::GetTraceLogger()

View file

@ -36,7 +36,6 @@ class NesDebugger final : public IDebugger
NesConsole* _console;
NesCpu* _cpu;
BaseNesPpu* _ppu;
BaseMapper* _mapper;
NesMemoryManager* _memoryManager;

View file

@ -15,12 +15,12 @@
NesEventManager::NesEventManager(Debugger *debugger, NesConsole* console)
{
_debugger = debugger;
_console = console;
_cpu = console->GetCpu();
_ppu = console->GetPpu();
_mapper = console->GetMapper();
_snapshotScanlineOffset = -1;
NesDefaultVideoFilter::GetFullPalette(_palette, console->GetNesConfig(), _ppu->GetPpuModel());
NesDefaultVideoFilter::GetFullPalette(_palette, console->GetNesConfig(), console->GetPpu()->GetPpuModel());
_ppuBuffer = new uint16_t[NesConstants::ScreenPixelCount];
memset(_ppuBuffer, 0, NesConstants::ScreenPixelCount * sizeof(uint16_t));
@ -33,11 +33,12 @@ NesEventManager::~NesEventManager()
void NesEventManager::AddEvent(DebugEventType type, MemoryOperationInfo& operation, int32_t breakpointId)
{
BaseNesPpu* ppu = _console->GetPpu();
DebugEventInfo evt = {};
evt.Type = type;
evt.Operation = operation;
evt.Scanline = (int16_t)_ppu->GetCurrentScanline();
evt.Cycle = (uint16_t)_ppu->GetCurrentCycle();
evt.Scanline = (int16_t)ppu->GetCurrentScanline();
evt.Cycle = (uint16_t)ppu->GetCurrentCycle();
evt.BreakpointId = breakpointId;
evt.ProgramCounter = _debugger->GetProgramCounter(CpuType::Nes, true);
evt.DmaChannel = -1;
@ -46,7 +47,7 @@ void NesEventManager::AddEvent(DebugEventType type, MemoryOperationInfo& operati
bool isWrite = operation.Type == MemoryOperationType::Write || operation.Type == MemoryOperationType::DmaWrite || operation.Type == MemoryOperationType::DummyWrite;
if(isWrite && (addr & 0xE000) == 0x2000) {
NesPpuState state;
_ppu->GetState(state);
ppu->GetState(state);
switch(addr & 0x07) {
case 4:
//OAM write
@ -85,7 +86,7 @@ void NesEventManager::AddEvent(DebugEventType type)
{
MemoryOperationInfo op = {};
if(type == DebugEventType::BgColorChange) {
op.Address = _ppu->GetCurrentBgColor();
op.Address = _console->GetPpu()->GetCurrentBgColor();
}
AddEvent(type, op, -1);
}
@ -193,18 +194,19 @@ void NesEventManager::ConvertScanlineCycleToRowColumn(int32_t& x, int32_t& y)
uint32_t NesEventManager::TakeEventSnapshot(bool forAutoRefresh)
{
BaseNesPpu* ppu = _console->GetPpu();
DebugBreakHelper breakHelper(_debugger);
auto lock = _lock.AcquireSafe();
uint16_t cycle = _ppu->GetCurrentCycle();
uint16_t scanline = _ppu->GetCurrentScanline() + 1;
uint16_t cycle = _console->GetPpu()->GetCurrentCycle();
uint16_t scanline = ppu->GetCurrentScanline() + 1;
if(scanline >= 240 || (scanline == 0 && cycle == 0)) {
memcpy(_ppuBuffer, _ppu->GetScreenBuffer(false), NesConstants::ScreenPixelCount * sizeof(uint16_t));
memcpy(_ppuBuffer, ppu->GetScreenBuffer(false), NesConstants::ScreenPixelCount * sizeof(uint16_t));
} else {
uint32_t offset = (NesConstants::ScreenWidth * scanline);
memcpy(_ppuBuffer, _ppu->GetScreenBuffer(false), offset * sizeof(uint16_t));
memcpy(_ppuBuffer + offset, _ppu->GetScreenBuffer(true) + offset, (NesConstants::ScreenPixelCount - offset) * sizeof(uint16_t));
memcpy(_ppuBuffer, ppu->GetScreenBuffer(false), offset * sizeof(uint16_t));
memcpy(_ppuBuffer + offset, ppu->GetScreenBuffer(true) + offset, (NesConstants::ScreenPixelCount - offset) * sizeof(uint16_t));
}
_snapshotCurrentFrame = _debugEvents;
@ -212,7 +214,7 @@ uint32_t NesEventManager::TakeEventSnapshot(bool forAutoRefresh)
_snapshotScanline = scanline;
_snapshotCycle = cycle;
_forAutoRefresh = forAutoRefresh;
_scanlineCount = _ppu->GetScanlineCount();
_scanlineCount = ppu->GetScanlineCount();
return _scanlineCount;
}

View file

@ -50,7 +50,7 @@ private:
NesEventViewerConfig _config = {};
NesCpu* _cpu = nullptr;
BaseNesPpu* _ppu = nullptr;
NesConsole* _console = nullptr;
BaseMapper* _mapper = nullptr;
Debugger* _debugger = nullptr;

View file

@ -1,15 +1,16 @@
#include "pch.h"
#include "NES/Debugger/NesTraceLogger.h"
#include "NES/NesPpu.h"
#include "NES/NesConsole.h"
#include "NES/BaseNesPpu.h"
#include "NES/NesTypes.h"
#include "Debugger/DisassemblyInfo.h"
#include "Debugger/Debugger.h"
#include "Debugger/DebugTypes.h"
#include "Utilities/HexUtilities.h"
NesTraceLogger::NesTraceLogger(Debugger* debugger, IDebugger* cpuDebugger, BaseNesPpu* ppu) : BaseTraceLogger(debugger, cpuDebugger, CpuType::Nes)
NesTraceLogger::NesTraceLogger(Debugger* debugger, IDebugger* cpuDebugger, NesConsole* console) : BaseTraceLogger(debugger, cpuDebugger, CpuType::Nes)
{
_ppu = ppu;
_console = console;
}
RowDataType NesTraceLogger::GetFormatTagType(string& tag)
@ -48,10 +49,11 @@ void NesTraceLogger::GetTraceRow(string &output, NesCpuState &cpuState, TraceLog
void NesTraceLogger::LogPpuState()
{
BaseNesPpu* ppu = _console->GetPpu();
_ppuState[_currentPos] = {
_ppu->GetCurrentCycle(),
_ppu->GetCurrentCycle(),
_ppu->GetCurrentScanline(),
_ppu->GetFrameCount()
ppu->GetCurrentCycle(),
ppu->GetCurrentCycle(),
ppu->GetCurrentScanline(),
ppu->GetFrameCount()
};
}

View file

@ -5,18 +5,18 @@
class DisassemblyInfo;
class Debugger;
class BaseNesPpu;
class NesConsole;
class NesTraceLogger : public BaseTraceLogger<NesTraceLogger, NesCpuState>
{
private:
BaseNesPpu* _ppu = nullptr;
NesConsole* _console = nullptr;
protected:
RowDataType GetFormatTagType(string& tag) override;
public:
NesTraceLogger(Debugger* debugger, IDebugger* cpuDebugger, BaseNesPpu* ppu);
NesTraceLogger(Debugger* debugger, IDebugger* cpuDebugger, NesConsole* console);
void GetTraceRow(string& output, NesCpuState& cpuState, TraceLogPpuState& ppuState, DisassemblyInfo& disassemblyInfo);
void LogPpuState();

View file

@ -0,0 +1,157 @@
#pragma once
#include "pch.h"
#include "NES/HdPacks/HdNesPpu.h"
#include "NES/HdPacks/HdNesPack.h"
#include "NES/HdPacks/HdPackBuilder.h"
#include "Shared/Video/VideoDecoder.h"
#include "Shared/RewindManager.h"
#include "Utilities/Serializer.h"
class HdBuilderPpu final : public NesPpu<HdBuilderPpu>
{
private:
HdPackBuilder* _hdPackBuilder = nullptr;
bool _needChrHash = false;
uint32_t _chrRamBankSize = 0;
uint32_t _chrRamIndexMask = 0;
vector<uint32_t> _bankHashes;
NesSpriteInfoEx _exSpriteInfo[64] = {};
NesTileInfoEx _previousTileEx = {};
NesTileInfoEx _currentTileEx = {};
NesTileInfoEx _nextTileEx = {};
public:
__forceinline bool RemoveSpriteLimit() { return _console->GetNesConfig().RemoveSpriteLimit; }
__forceinline bool UseAdaptiveSpriteLimit() { return _console->GetNesConfig().AdaptiveSpriteLimit; }
void* OnBeforeSendFrame() { return nullptr; }
__forceinline void StoreSpriteInformation(bool verticalMirror, uint16_t tileAddr, uint8_t lineOffset)
{
NesSpriteInfoEx& info = _exSpriteInfo[_spriteIndex];
info.TileAddr = tileAddr;
info.AbsoluteTileAddr = _mapper->GetPpuAbsoluteAddress(info.TileAddr).Address;
info.VerticalMirror = verticalMirror;
info.OffsetY = lineOffset;
}
__forceinline void StoreTileInformation()
{
_previousTileEx = _currentTileEx;
_currentTileEx = _nextTileEx;
uint8_t tileIndex = ReadVram(GetNameTableAddr());
uint16_t tileAddr = (tileIndex << 4) | (_videoRamAddr >> 12) | _control.BackgroundPatternAddr;
_nextTileEx.OffsetY = _videoRamAddr >> 12;
_nextTileEx.TileAddr = tileAddr;
_nextTileEx.AbsoluteTileAddr = _mapper->GetPpuAbsoluteAddress(tileAddr).Address;
}
__forceinline void ProcessScanline()
{
ProcessScanlineImpl();
}
void DrawPixel()
{
if(IsRenderingEnabled() || ((_videoRamAddr & 0x3F00) != 0x3F00)) {
BaseMapper* mapper = _console->GetMapper();
bool isChrRam = !mapper->HasChrRom();
_lastSprite = nullptr;
uint32_t color = GetPixelColor();
_currentOutputBuffer[(_scanline << 8) + _cycle - 1] = _paletteRam[color & 0x03 ? color : 0];
uint32_t backgroundColor = 0;
if(_mask.BackgroundEnabled && _cycle > _minimumDrawBgCycle) {
backgroundColor = (((_lowBitShift << _xScroll) & 0x8000) >> 15) | (((_highBitShift << _xScroll) & 0x8000) >> 14);
}
if(_needChrHash ) {
uint16_t addr = 0;
_bankHashes.clear();
while(addr < 0x2000) {
uint32_t hash = 0;
for(uint16_t i = 0; i < _chrRamBankSize; i++) {
hash += mapper->DebugReadVram(i + addr);
hash = (hash << 1) | (hash >> 31);
}
_bankHashes.push_back(hash);
addr += _chrRamBankSize;
}
_needChrHash = false;
}
bool hasBgSprite = false;
if(_lastSprite && _mask.SpritesEnabled) {
uint8_t spriteIndex = (uint8_t)((_lastSprite - _spriteTiles) / sizeof(NesSpriteInfo));
NesSpriteInfoEx& spriteInfoEx = _exSpriteInfo[spriteIndex];
if(backgroundColor == 0) {
for(uint8_t i = 0; i < _spriteCount; i++) {
if(_spriteTiles[i].BackgroundPriority) {
hasBgSprite = true;
break;
}
}
}
if(spriteInfoEx.AbsoluteTileAddr >= 0) {
HdPpuTileInfo sprite = {};
sprite.TileIndex = (isChrRam ? (spriteInfoEx.TileAddr & _chrRamIndexMask) : spriteInfoEx.AbsoluteTileAddr) / 16;
sprite.PaletteColors = ReadPaletteRam(_lastSprite->PaletteOffset + 3) | (ReadPaletteRam(_lastSprite->PaletteOffset + 2) << 8) | (ReadPaletteRam(_lastSprite->PaletteOffset + 1) << 16) | 0xFF000000;
sprite.IsChrRamTile = isChrRam;
mapper->CopyChrTile(spriteInfoEx.AbsoluteTileAddr & 0xFFFFFFF0, sprite.TileData);
_hdPackBuilder->ProcessTile(_cycle - 1, _scanline, spriteInfoEx.AbsoluteTileAddr, sprite, mapper, false, _bankHashes[spriteInfoEx.TileAddr / _chrRamBankSize], false);
}
}
if(_mask.BackgroundEnabled) {
bool usePrev = (_xScroll + ((_cycle - 1) & 0x07) < 8);
uint8_t tilePalette = usePrev ? _previousTilePalette : _currentTilePalette;
NesTileInfoEx& lastTileEx = usePrev ? _previousTileEx : _currentTileEx;
//TileInfo* lastTile = &((_xScroll + ((_cycle - 1) & 0x07) < 8) ? _previousTile : _currentTile);
if(lastTileEx.AbsoluteTileAddr >= 0) {
HdPpuTileInfo tile = {};
tile.TileIndex = (isChrRam ? (lastTileEx.TileAddr & _chrRamIndexMask) : lastTileEx.AbsoluteTileAddr) / 16;
tile.PaletteColors = ReadPaletteRam(tilePalette + 3) | (ReadPaletteRam(tilePalette + 2) << 8) | (ReadPaletteRam(tilePalette + 1) << 16) | (ReadPaletteRam(0) << 24);
tile.IsChrRamTile = isChrRam;
mapper->CopyChrTile(lastTileEx.AbsoluteTileAddr & 0xFFFFFFF0, tile.TileData);
_hdPackBuilder->ProcessTile(_cycle - 1, _scanline, lastTileEx.AbsoluteTileAddr, tile, mapper, false, _bankHashes[lastTileEx.TileAddr / _chrRamBankSize], hasBgSprite);
}
}
} else {
//"If the current VRAM address points in the range $3F00-$3FFF during forced blanking, the color indicated by this palette location will be shown on screen instead of the backdrop color."
_currentOutputBuffer[(_scanline << 8) + _cycle - 1] = _paletteRam[_videoRamAddr & 0x1F];
}
}
void WriteRAM(uint16_t addr, uint8_t value)
{
if(GetRegisterID(addr) == PpuRegisters::VideoMemoryData) {
if(_videoRamAddr < 0x2000) {
_needChrHash = true;
}
}
NesPpu::WriteRam(addr, value);
}
void Serialize(Serializer& s)
{
NesPpu::Serialize(s);
if(!s.IsSaving()) {
_needChrHash = true;
}
}
public:
HdBuilderPpu(NesConsole* console, HdPackBuilder* hdPackBuilder, uint32_t chrRamBankSize) : NesPpu(console)
{
_hdPackBuilder = hdPackBuilder;
_chrRamBankSize = chrRamBankSize;
_chrRamIndexMask = chrRamBankSize - 1;
_needChrHash = true;
}
};

View file

@ -7,22 +7,23 @@
#include "NES/BaseMapper.h"
#include "NES/HdPacks/HdData.h"
struct NesSpriteInfoEx
{
uint32_t AbsoluteTileAddr;
uint16_t TileAddr;
bool VerticalMirror;
uint8_t OffsetY;
};
struct NesTileInfoEx
{
uint32_t AbsoluteTileAddr;
uint16_t TileAddr;
uint8_t OffsetY;
};
class HdNesPpu final : public NesPpu<HdNesPpu>
{
struct NesSpriteInfoEx
{
uint32_t AbsoluteTileAddr;
uint16_t TileAddr;
bool VerticalMirror;
uint8_t OffsetY;
};
struct NesTileInfoEx
{
uint32_t AbsoluteTileAddr;
uint8_t OffsetY;
};
HdScreenInfo* _screenInfo[2] = {};
HdScreenInfo* _info = nullptr;
uint32_t _version = 0;

View file

@ -0,0 +1,450 @@
#include "pch.h"
#include <algorithm>
#include "NES/HdPacks/HdPackBuilder.h"
#include "NES/HdPacks/HdNesPack.h"
#include "NES/BaseMapper.h"
#include "NES/BaseNesPpu.h"
#include "NES/NesConstants.h"
#include "NES/HdPacks/HdPackLoader.h"
#include "NES/NesDefaultVideoFilter.h"
#include "Utilities/xBRZ/xbrz.h"
#include "Utilities/HQX/hqx.h"
#include "Utilities/Scale2x/scalebit.h"
#include "Utilities/KreedSaiEagle/SaiEagle.h"
#include "Utilities/FolderUtilities.h"
#include "Utilities/PNGHelper.h"
#include "Utilities/HexUtilities.h"
#include "Utilities/VirtualFile.h"
#include "Shared/Emulator.h"
#include "Shared/EmuSettings.h"
HdPackBuilder::HdPackBuilder(Emulator* emu, PpuModel ppuModel, bool isChrRam, HdPackBuilderOptions options)
{
_emu = emu;
_isChrRam = isChrRam;
_saveFolder = options.SaveFolder;
options.SaveFolder = nullptr;
_options = options;
NesDefaultVideoFilter::GetFullPalette(_palette, _emu->GetSettings()->GetNesConfig(), ppuModel);
string existingPackDefinition = FolderUtilities::CombinePath(_saveFolder, "hires.txt");
if(ifstream(existingPackDefinition)) {
HdPackLoader::LoadHdNesPack(existingPackDefinition, _hdData);
for(unique_ptr<HdPackTileInfo> &tile : _hdData.Tiles) {
//Mark the tiles in the first PNGs as higher usage (preserves order when adding new tiles to an existing set)
AddTile(tile.get(), 0xFFFFFFFF - tile->BitmapIndex);
}
if(_hdData.Scale != _options.Scale) {
_options.FilterType = ScaleFilterType::Prescale;
}
} else {
_hdData.Scale = _options.Scale;
}
_romName = FolderUtilities::GetFilename(_emu->GetRomInfo().RomFile.GetFileName(), false);
}
HdPackBuilder::~HdPackBuilder()
{
SaveHdPack();
}
void HdPackBuilder::AddTile(HdPackTileInfo *tile, uint32_t usageCount)
{
bool isTileBlank = _options.GroupBlankTiles ? tile->Blank : false;
int chrBankId = isTileBlank ? 0xFFFFFFFF : tile->ChrBankId;
int palette = isTileBlank ? _blankTilePalette : tile->PaletteColors;
if(_tilesByChrBankByPalette.find(chrBankId) == _tilesByChrBankByPalette.end()) {
_tilesByChrBankByPalette[chrBankId] = std::map<uint32_t, vector<HdPackTileInfo*>>();
}
std::map<uint32_t, vector<HdPackTileInfo*>> &paletteMap = _tilesByChrBankByPalette[chrBankId];
if(paletteMap.find(palette) == paletteMap.end()) {
paletteMap[palette] = vector<HdPackTileInfo*>(256, nullptr);
}
if(isTileBlank) {
paletteMap[palette][_blankTileIndex] = tile;
_blankTileIndex++;
if(_blankTileIndex == _options.ChrRamBankSize / 16) {
_blankTileIndex = 0;
_blankTilePalette++;
}
} else {
if(tile->TileIndex >= 0) {
paletteMap[palette][tile->TileIndex % 256] = tile;
} else {
//FIXME: This will result in data loss if more than 256 tiles of the same palette exist in the hires.txt file
//Currently this way to prevent issues when loading a CHR RAM HD pack into the recorder (because TileIndex is -1 in that case)
for(int i = 0; i < 256; i++) {
if(paletteMap[palette][i] == nullptr) {
paletteMap[palette][i] = tile;
break;
}
}
}
}
_tilesByKey[tile->GetKey(false)] = tile;
_tileUsageCount[tile->GetKey(false)] = usageCount;
}
void HdPackBuilder::ProcessTile(uint32_t x, uint32_t y, uint16_t tileAddr, HdPpuTileInfo &tile, BaseMapper *mapper, bool isSprite, uint32_t chrBankHash, bool transparencyRequired)
{
if(_options.IgnoreOverscan) {
OverscanDimensions overscan = _emu->GetSettings()->GetOverscan();
if(x < overscan.Left || y < overscan.Top || (NesConstants::ScreenWidth - x - 1) < overscan.Right || (NesConstants::ScreenHeight - y - 1) < overscan.Bottom) {
//Ignore tiles inside overscan
return;
}
}
auto result = _tileUsageCount.find(tile.GetKey(false));
if(result == _tileUsageCount.end()) {
//Check to see if a default tile matches
result = _tileUsageCount.find(tile.GetKey(true));
}
if(result == _tileUsageCount.end()) {
//First time seeing this tile/palette combination, store it
HdPackTileInfo* hdTile = new HdPackTileInfo();
hdTile->PaletteColors = tile.PaletteColors;
hdTile->TileIndex = tile.TileIndex;
hdTile->DefaultTile = false;
hdTile->IsChrRamTile = _isChrRam;
hdTile->Brightness = 255;
hdTile->ChrBankId = _isChrRam ? chrBankHash : (tileAddr / 16 / 256);
hdTile->TransparencyRequired = transparencyRequired;
memcpy(hdTile->TileData, tile.TileData, 16);
_hdData.Tiles.push_back(unique_ptr<HdPackTileInfo>(hdTile));
AddTile(hdTile, 1);
} else {
if(transparencyRequired) {
auto existingTile = _tilesByKey.find(tile.GetKey(false));
if(existingTile != _tilesByKey.end()) {
existingTile->second->TransparencyRequired = true;
}
}
if(result->second < 0x7FFFFFFF) {
//Increase usage count
result->second++;
}
}
}
void HdPackBuilder::GenerateHdTile(HdPackTileInfo *tile)
{
uint32_t hdScale = _hdData.Scale;
vector<uint32_t> originalTile = tile->ToRgb(_palette);
vector<uint32_t> hdTile(8 * 8 * hdScale*hdScale, 0);
switch(_options.FilterType) {
case ScaleFilterType::HQX:
hqx(hdScale, originalTile.data(), hdTile.data(), 8, 8);
break;
case ScaleFilterType::Prescale:
hdTile.clear();
for(uint8_t i = 0; i < 8 * hdScale; i++) {
for(uint8_t j = 0; j < 8 * hdScale; j++) {
hdTile.push_back(originalTile[i/hdScale*8+j/hdScale]);
}
}
break;
case ScaleFilterType::Scale2x:
scale(hdScale, hdTile.data(), 8 * sizeof(uint32_t) * hdScale, originalTile.data(), 8 * sizeof(uint32_t), 4, 8, 8);
break;
case ScaleFilterType::_2xSai:
twoxsai_generic_xrgb8888(8, 8, originalTile.data(), 8, hdTile.data(), 8 * hdScale);
break;
case ScaleFilterType::Super2xSai:
supertwoxsai_generic_xrgb8888(8, 8, originalTile.data(), 8, hdTile.data(), 8 * hdScale);
break;
case ScaleFilterType::SuperEagle:
supereagle_generic_xrgb8888(8, 8, originalTile.data(), 8, hdTile.data(), 8 * hdScale);
break;
case ScaleFilterType::xBRZ:
xbrz::scale(hdScale, originalTile.data(), hdTile.data(), 8, 8, xbrz::ColorFormat::ARGB);
break;
}
tile->HdTileData = hdTile;
}
void HdPackBuilder::DrawTile(HdPackTileInfo *tile, int tileNumber, uint32_t *pngBuffer, int pageNumber, bool containsSpritesOnly)
{
if(tile->HdTileData.empty()) {
GenerateHdTile(tile);
tile->UpdateFlags();
}
if(containsSpritesOnly && _options.UseLargeSprites) {
int row = tileNumber / 16;
int column = tileNumber % 16;
int newColumn = column / 2 + ((row & 1) ? 8 : 0);
int newRow = (row & 0xFE) + ((column & 1) ? 1 : 0);
tileNumber = newRow * 16 + newColumn;
}
tileNumber += pageNumber * (256 / (0x1000 / _options.ChrRamBankSize));
int tileDimension = 8 * _hdData.Scale;
int x = tileNumber % 16 * tileDimension;
int y = tileNumber / 16 * tileDimension;
tile->X = x;
tile->Y = y;
int pngWidth = 128 * _hdData.Scale;
int pngPos = y * pngWidth + x;
int tilePos = 0;
for(uint8_t i = 0; i < tileDimension; i++) {
for(uint8_t j = 0; j < tileDimension; j++) {
pngBuffer[pngPos] = tile->HdTileData[tilePos++];
pngPos++;
}
pngPos += pngWidth - tileDimension;
}
}
void HdPackBuilder::SaveHdPack()
{
FolderUtilities::CreateFolder(_saveFolder);
stringstream pngRows;
stringstream tileRows;
stringstream ss;
int pngIndex = 0;
ss << "<ver>" << std::to_string(HdNesPack::CurrentVersion) << std::endl;
ss << "<scale>" << _hdData.Scale << std::endl;
ss << "<supportedRom>" << _emu->GetRomInfo().RomFile.GetSha1Hash() << std::endl;
if(_options.IgnoreOverscan) {
OverscanDimensions overscan = _emu->GetSettings()->GetOverscan();
ss << "<overscan>" << overscan.Top << "," << overscan.Right << "," << overscan.Bottom << "," << overscan.Left << std::endl;
}
int tileDimension = 8 * _hdData.Scale;
int pngDimension = 16 * tileDimension;
int pngBufferSize = pngDimension * pngDimension;
uint32_t* pngBuffer = new uint32_t[pngBufferSize];
int maxPageNumber = 0x1000 / _options.ChrRamBankSize;
int pageNumber = 0;
bool pngEmpty = true;
int pngNumber = 0;
for(int i = 0; i < pngBufferSize; i++) {
pngBuffer[i] = 0xFFFF00FF;
}
auto savePng = [&tileRows, &pngRows, &ss, &pngBuffer, &pngDimension, &pngIndex, &pngBufferSize, &pngEmpty, &pngNumber, this](uint32_t chrBankId) {
if(!pngEmpty) {
string pngName;
if(_isChrRam) {
pngName = "Chr_" + std::to_string(pngNumber) + ".png";
} else {
pngName = "Chr_" + HexUtilities::ToHex(chrBankId) + "_" + std::to_string(pngNumber) + ".png";
}
tileRows << std::endl << "#" << pngName << std::endl;
tileRows << pngRows.str();
pngRows = stringstream();
ss << "<img>" << pngName << std::endl;
PNGHelper::WritePNG(FolderUtilities::CombinePath(_saveFolder, pngName), pngBuffer, pngDimension, pngDimension, 32);
pngNumber++;
pngIndex++;
for(int i = 0; i < pngBufferSize; i++) {
pngBuffer[i] = 0xFFFF00FF;
}
pngEmpty = true;
}
};
for(std::pair<const uint32_t, std::map<uint32_t, vector<HdPackTileInfo*>>> &kvp : _tilesByChrBankByPalette) {
if(_options.SortByUsageFrequency) {
for(int i = 0; i < 256; i++) {
vector<std::pair<uint32_t, HdPackTileInfo*>> tiles;
for(std::pair<const uint32_t, vector<HdPackTileInfo*>> &paletteMap : kvp.second) {
if(paletteMap.second[i]) {
tiles.push_back({ _tileUsageCount[paletteMap.second[i]->GetKey(false)], paletteMap.second[i] });
}
}
std::sort(tiles.begin(), tiles.end(), [=](std::pair<uint32_t, HdPackTileInfo*> &a, std::pair<uint32_t, HdPackTileInfo*> &b) {
return a.first > b.first;
});
size_t j = 0;
for(std::pair<const uint32_t, vector<HdPackTileInfo*>> &paletteMap : kvp.second) {
if(j < tiles.size()) {
paletteMap.second[i] = tiles[j].second;
j++;
} else {
paletteMap.second[i] = nullptr;
}
}
}
}
if(!_isChrRam) {
pngNumber = 0;
}
for(std::pair<const uint32_t, vector<HdPackTileInfo*>> &tileKvp : kvp.second) {
bool pageEmpty = true;
bool spritesOnly = true;
for(HdPackTileInfo* tileInfo : tileKvp.second) {
if(tileInfo && !tileInfo->IsSpriteTile()) {
spritesOnly = false;
}
}
for(int i = 0; i < 256; i++) {
HdPackTileInfo* tileInfo = tileKvp.second[i];
if(tileInfo) {
DrawTile(tileInfo, i, pngBuffer, pageNumber, spritesOnly);
pngRows << tileInfo->ToString(pngIndex) << std::endl;
pageEmpty = false;
pngEmpty = false;
}
}
if(!pageEmpty) {
pageNumber++;
if(pageNumber == maxPageNumber) {
savePng(kvp.first);
pageNumber = 0;
}
}
}
}
savePng(-1);
for(unique_ptr<HdPackCondition> &condition : _hdData.Conditions) {
if(!condition->IsExcludedFromFile()) {
ss << condition->ToString() << std::endl;
}
}
for(HdBackgroundInfo &bgInfo : _hdData.Backgrounds) {
ss << bgInfo.ToString() << std::endl;
}
for(auto &bgmInfo : _hdData.BgmFilesById) {
ss << "<bgm>" << std::to_string(bgmInfo.first >> 8) << "," << std::to_string(bgmInfo.first & 0xFF) << "," << VirtualFile(bgmInfo.second).GetFileName() << std::endl;
}
for(auto &sfxInfo : _hdData.SfxFilesById) {
ss << "<sfx>" << std::to_string(sfxInfo.first >> 8) << "," << std::to_string(sfxInfo.first & 0xFF) << "," << VirtualFile(sfxInfo.second).GetFileName() << std::endl;
}
for(auto &patchInfo : _hdData.PatchesByHash) {
ss << "<patch>" << VirtualFile(patchInfo.second).GetFileName() << "," << patchInfo.first << std::endl;
}
if(_hdData.OptionFlags != 0) {
ss << "<options>";
if(_hdData.OptionFlags & (int)HdPackOptions::NoSpriteLimit) {
ss << "disableSpriteLimit,";
}
if(_hdData.OptionFlags & (int)HdPackOptions::AlternateRegisterRange) {
ss << "alternateRegisterRange,";
}
if(_hdData.OptionFlags & (int)HdPackOptions::DisableCache) {
ss << "disableCache,";
}
}
ss << tileRows.str();
ofstream hiresFile(FolderUtilities::CombinePath(_saveFolder, "hires.txt"), ios::out);
hiresFile << ss.str();
hiresFile.close();
delete[] pngBuffer;
}
/*
void HdPackBuilder::GetChrBankList(uint32_t *banks)
{
ConsolePauseHelper helper(_instance->_console.get());
for(std::pair<const uint32_t, std::map<uint32_t, vector<HdPackTileInfo*>>> &kvp : _instance->_tilesByChrBankByPalette) {
*banks = kvp.first;
banks++;
}
*banks = -1;
}
void HdPackBuilder::GetBankPreview(uint32_t bankNumber, uint32_t pageNumber, uint32_t *rgbBuffer)
{
ConsolePauseHelper helper(_instance->_console.get());
for(uint32_t i = 0; i < 128 * 128 * _instance->_hdData.Scale*_instance->_hdData.Scale; i++) {
rgbBuffer[i] = 0xFF666666;
}
auto result = _instance->_tilesByChrBankByPalette.find(bankNumber);
if(result != _instance->_tilesByChrBankByPalette.end()) {
std::map<uint32_t, vector<HdPackTileInfo*>> bankData = result->second;
if(_instance->_flags & HdPackRecordFlags::SortByUsageFrequency) {
for(int i = 0; i < 256; i++) {
vector<std::pair<uint32_t, HdPackTileInfo*>> tiles;
for(std::pair<const uint32_t, vector<HdPackTileInfo*>> &pageData : bankData) {
if(pageData.second[i]) {
tiles.push_back({ _instance->_tileUsageCount[pageData.second[i]->GetKey(false)], pageData.second[i] });
}
}
std::sort(tiles.begin(), tiles.end(), [=](std::pair<uint32_t, HdPackTileInfo*> &a, std::pair<uint32_t, HdPackTileInfo*> &b) {
return a.first > b.first;
});
size_t j = 0;
for(std::pair<const uint32_t, vector<HdPackTileInfo*>> &pageData : bankData) {
if(j < tiles.size()) {
pageData.second[i] = tiles[j].second;
j++;
} else {
pageData.second[i] = nullptr;
}
}
}
}
bool spritesOnly = true;
for(HdPackTileInfo* tileInfo : (*bankData.begin()).second) {
if(tileInfo && !tileInfo->IsSpriteTile()) {
spritesOnly = false;
}
}
for(int i = 0; i < 256; i++) {
HdPackTileInfo* tileInfo = (*bankData.begin()).second[i];
if(tileInfo) {
_instance->DrawTile(tileInfo, i, (uint32_t*)rgbBuffer, 0, spritesOnly);
}
}
}
}
*/

View file

@ -0,0 +1,56 @@
#pragma once
#include "pch.h"
#include "NES/HdPacks/HdData.h"
#include "NES/NesTypes.h"
#include "Shared/SettingTypes.h"
#include <map>
class Emulator;
class BaseMapper;
struct HdPackBuilderOptions
{
char* SaveFolder;
ScaleFilterType FilterType;
uint32_t Scale;
uint32_t ChrRamBankSize;
bool UseLargeSprites;
bool SortByUsageFrequency;
bool GroupBlankTiles;
bool IgnoreOverscan;
};
class HdPackBuilder
{
private:
Emulator* _emu = nullptr;
HdPackData _hdData;
unordered_map<HdTileKey, uint32_t> _tileUsageCount;
unordered_map<HdTileKey, HdPackTileInfo*> _tilesByKey;
std::map<uint32_t, std::map<uint32_t, vector<HdPackTileInfo*>>> _tilesByChrBankByPalette;
bool _isChrRam = false;
string _saveFolder;
string _romName;
HdPackBuilderOptions _options = {};
uint32_t _palette[512] = {};
//Used to group blank tiles together
uint32_t _blankTileIndex = 0;
int _blankTilePalette = 0;
void AddTile(HdPackTileInfo *tile, uint32_t usageCount);
void GenerateHdTile(HdPackTileInfo *tile);
void DrawTile(HdPackTileInfo *tile, int tileIndex, uint32_t* pngBuffer, int pageNumber, bool containsSpritesOnly);
public:
HdPackBuilder(Emulator* emu, PpuModel ppuModel, bool isChrRam, HdPackBuilderOptions options);
~HdPackBuilder();
void ProcessTile(uint32_t x, uint32_t y, uint16_t tileAddr, HdPpuTileInfo& tile, BaseMapper* mapper, bool isSprite, uint32_t chrBankHash, bool transparencyRequired);
void SaveHdPack();
//static void GetChrBankList(uint32_t *banks);
//static void GetBankPreview(uint32_t bankNumber, uint32_t pageNumber, uint32_t *rgbBuffer);
};

View file

@ -11,7 +11,6 @@ class MMC3 : public BaseMapper
{
private:
NesCpu* _cpu = nullptr;
BaseNesPpu* _ppu = nullptr;
uint8_t _currentRegister = 0;
@ -197,7 +196,6 @@ protected:
void InitMapper() override
{
_cpu = _console->GetCpu();
_ppu = _console->GetPpu();
//Force MMC3A irqs for boards that are known to use the A revision.
//Some MMC3B boards also have the A behavior, but currently no way to tell them apart.
@ -305,7 +303,7 @@ protected:
public:
void NotifyVramAddressChange(uint16_t addr) override
{
if(_a12Watcher.UpdateVramAddress(addr, _ppu->GetFrameCycle()) == A12StateChange::Rise) {
if(_a12Watcher.UpdateVramAddress(addr, _console->GetPpu()->GetFrameCycle()) == A12StateChange::Rise) {
uint32_t count = _irqCounter;
if(_irqCounter == 0 || _irqReload) {
_irqCounter = _irqReloadValue;

View file

@ -13,6 +13,8 @@
#include "NES/HdPacks/HdData.h"
#include "NES/HdPacks/HdNesPpu.h"
#include "NES/HdPacks/HdPackLoader.h"
#include "NES/HdPacks/HdPackBuilder.h"
#include "NES/HdPacks/HdBuilderPpu.h"
#include "NES/HdPacks/HdVideoFilter.h"
#include "NES/NesDefaultVideoFilter.h"
#include "NES/NesNtscFilter.h"
@ -22,6 +24,8 @@
#include "NES/Mappers/NSF/NsfMapper.h"
#include "NES/Mappers/FDS/Fds.h"
#include "Shared/Emulator.h"
#include "Shared/Audio/SoundMixer.h"
#include "Shared/SaveStateManager.h"
#include "Shared/CheatManager.h"
#include "Shared/Movies/MovieManager.h"
#include "Shared/BaseControlManager.h"
@ -400,7 +404,7 @@ ShortcutState NesConsole::IsShortcutAllowed(EmulatorShortcut shortcut, uint32_t
BaseVideoFilter* NesConsole::GetVideoFilter()
{
if(_hdData) {
if(_hdData && !_hdPackBuilder) {
return new HdVideoFilter(_emu, _hdData.get());
} else if(GetRomFormat() == RomFormat::Nsf) {
return new NesDefaultVideoFilter(_emu);
@ -610,4 +614,59 @@ DipSwitchInfo NesConsole::GetDipSwitchInfo()
info.DipSwitchCount = _mapper->GetMapperDipSwitchCount();
info.DatabaseId = _mapper->GetRomInfo().Hash.PrgCrc32;
return info;
}
void NesConsole::ProcessNotification(ConsoleNotificationType type, void* parameter)
{
if(type == ConsoleNotificationType::ExecuteShortcut) {
ExecuteShortcutParams* params = (ExecuteShortcutParams*)parameter;
switch(params->Shortcut) {
default: break;
case EmulatorShortcut::StartRecordHdPack: StartRecordingHdPack(*(HdPackBuilderOptions*)params->ParamPtr); break;
case EmulatorShortcut::StopRecordHdPack: StopRecordingHdPack(); break;
}
}
}
void NesConsole::StartRecordingHdPack(HdPackBuilderOptions options)
{
auto lock = _emu->AcquireLock();
std::stringstream saveState;
_emu->Serialize(saveState, false, 0);
_hdPackBuilder.reset();
_hdPackBuilder.reset(new HdPackBuilder(_emu, _ppu->GetPpuModel(), !_mapper->HasChrRom(), options));
_memoryManager->UnregisterIODevice(_ppu.get());
_ppu.reset(new HdBuilderPpu(this, _hdPackBuilder.get(), options.ChrRamBankSize));
_memoryManager->RegisterIODevice(_ppu.get());
_emu->Deserialize(saveState, SaveStateManager::FileFormatVersion, false);
_emu->GetSoundMixer()->StopAudio();
_emu->GetVideoDecoder()->ForceFilterUpdate();
}
void NesConsole::StopRecordingHdPack()
{
if(_hdPackBuilder) {
auto lock = _emu->AcquireLock();
std::stringstream saveState;
_emu->Serialize(saveState, false, 0);
_memoryManager->UnregisterIODevice(_ppu.get());
if(_hdData && (!_hdData->Tiles.empty() || !_hdData->Backgrounds.empty())) {
_ppu.reset(new HdNesPpu(this, _hdData.get()));
} else {
_ppu.reset(new DefaultNesPpu(this));
}
_memoryManager->RegisterIODevice(_ppu.get());
_hdPackBuilder.reset();
_emu->Deserialize(saveState, SaveStateManager::FileFormatVersion, false);
_emu->GetSoundMixer()->StopAudio();
_emu->GetVideoDecoder()->ForceFilterUpdate();
}
}

View file

@ -17,7 +17,9 @@ class NesSoundMixer;
class BaseVideoFilter;
class BaseControlManager;
class HdAudioDevice;
class HdPackBuilder;
struct HdPackData;
struct HdPackBuilderOptions;
enum class DebugEventType;
enum class EventType;
@ -43,6 +45,7 @@ private:
unique_ptr<HdPackData> _hdData;
unique_ptr<HdAudioDevice> _hdAudioDevice;
unique_ptr<HdPackBuilder> _hdPackBuilder;
ConsoleRegion _region = ConsoleRegion::Auto;
@ -53,6 +56,9 @@ private:
void InitializeInputDevices(GameInputType inputType, GameSystem system);
void StartRecordingHdPack(HdPackBuilderOptions options);
void StopRecordingHdPack();
public:
NesConsole(Emulator* emulator);
~NesConsole();
@ -113,4 +119,6 @@ public:
void ProcessCheatCode(InternalCheatCode& code, uint32_t addr, uint8_t& value) override;
void InitializeRam(void* data, uint32_t length);
DipSwitchInfo GetDipSwitchInfo() override;
void ProcessNotification(ConsoleNotificationType type, void* parameter) override;
};

View file

@ -15,6 +15,7 @@
#include "NES/DefaultNesPpu.h"
#include "NES/NsfPpu.h"
#include "NES/HdPacks/HdNesPpu.h"
#include "NES/HdPacks/HdBuilderPpu.h"
#include "Debugger/Debugger.h"
#include "Shared/EmuSettings.h"
@ -1473,3 +1474,8 @@ template NesPpu<HdNesPpu>::NesPpu(NesConsole* console);
template uint16_t* NesPpu<HdNesPpu>::GetScreenBuffer(bool previousBuffer);
template void NesPpu<HdNesPpu>::Exec();
template uint32_t NesPpu<HdNesPpu>::GetPixelBrightness(uint8_t x, uint8_t y);
template NesPpu<HdBuilderPpu>::NesPpu(NesConsole* console);
template uint16_t* NesPpu<HdBuilderPpu>::GetScreenBuffer(bool previousBuffer);
template void NesPpu<HdBuilderPpu>::Exec();
template uint32_t NesPpu<HdBuilderPpu>::GetPixelBrightness(uint8_t x, uint8_t y);

View file

@ -514,6 +514,7 @@ void Emulator::InitConsole(unique_ptr<IConsole>& newConsole, ConsoleMemoryInfo o
}
_console.reset(newConsole);
_notificationManager->RegisterNotificationListener(_console.lock());
}
template<typename T>

View file

@ -3,6 +3,7 @@
#include "Utilities/ISerializable.h"
#include "Core/Debugger/DebugTypes.h"
#include "Shared/Audio/AudioPlayerTypes.h"
#include "Shared/Interfaces/INotificationListener.h"
#include "Shared/RomInfo.h"
#include "Shared/TimingInfo.h"
@ -42,7 +43,7 @@ enum class ShortcutState
Default = 2
};
class IConsole : public ISerializable
class IConsole : public ISerializable, public INotificationListener
{
public:
virtual ~IConsole() {}
@ -97,5 +98,7 @@ public:
virtual void GetConsoleState(BaseState& state, ConsoleType consoleType) = 0;
virtual void ProcessCheatCode(InternalCheatCode& code, uint32_t addr, uint8_t& value) {}
virtual void ProcessNotification(ConsoleNotificationType type, void* parameter) {}
};

View file

@ -45,4 +45,5 @@ struct ExecuteShortcutParams
{
EmulatorShortcut Shortcut;
uint32_t Param;
void* ParamPtr;
};

View file

@ -781,6 +781,8 @@ enum class EmulatorShortcut
VsInsertCoin2,
VsInsertCoin3,
VsInsertCoin4,
StartRecordHdPack,
StopRecordHdPack,
ShortcutCount,
};

View file

@ -71,12 +71,13 @@ void VideoDecoder::UpdateVideoFilter()
VideoFilterType newFilter = _emu->GetSettings()->GetVideoConfig().VideoFilter;
ConsoleType consoleType = _emu->GetConsoleType();
if(_videoFilterType != newFilter || _videoFilter == nullptr || _consoleType != consoleType) {
if(_videoFilterType != newFilter || _videoFilter == nullptr || _consoleType != consoleType || _forceFilterUpdate) {
_videoFilterType = newFilter;
_consoleType = consoleType;
_videoFilter.reset(_emu->GetVideoFilter());
_scaleFilter = ScaleFilter::GetScaleFilter(_videoFilterType);
_forceFilterUpdate = false;
}
uint32_t screenRotation = _emu->GetSettings()->GetVideoConfig().ScreenRotation;

View file

@ -26,6 +26,7 @@ private:
atomic<bool> _frameChanged;
atomic<bool> _stopFlag;
uint32_t _frameCount = 0;
bool _forceFilterUpdate = false;
double _lastAspectRatio = 0.0;
@ -51,6 +52,8 @@ public:
void DecodeFrame(bool synchronous = false);
void TakeScreenshot();
void TakeScreenshot(std::stringstream &stream);
void ForceFilterUpdate() { _forceFilterUpdate = true; }
uint32_t GetFrameCount();
FrameInfo GetBaseFrameInfo(bool removeOverscan);

View file

@ -244,6 +244,8 @@ extern "C" {
return { 256, 240 };
}
DllExport uint32_t __stdcall GetGameMemorySize(MemoryType type) { return _emu->GetMemory(type).Size; }
DllExport void __stdcall ClearCheats() { _emu->GetCheatManager()->ClearCheats(); }
DllExport void __stdcall SetCheats(CheatCode codes[], uint32_t length) { _emu->GetCheatManager()->SetCheats(codes, length); }
DllExport bool __stdcall GetConvertedCheat(CheatCode input, InternalCheatCode& output) { return _emu->GetCheatManager()->GetConvertedCheat(input, output); }

View file

@ -35,6 +35,7 @@ namespace Mesen.Config
[Reactive] public RecentItems RecentFiles { get; set; } = new();
[Reactive] public VideoRecordConfig VideoRecord { get; set; } = new();
[Reactive] public MovieRecordConfig MovieRecord { get; set; } = new();
[Reactive] public HdPackBuilderConfig HdPackBuilder { get; set; } = new();
[Reactive] public CheatWindowConfig Cheats { get; set; } = new();
[Reactive] public NetplayConfig Netplay { get; set; } = new();
[Reactive] public HistoryViewerConfig HistoryViewer { get; set; } = new();

View file

@ -0,0 +1,57 @@
using Mesen.Interop;
using ReactiveUI.Fody.Helpers;
using System.Runtime.InteropServices;
using System;
namespace Mesen.Config
{
public class HdPackBuilderConfig : BaseConfig<HdPackBuilderConfig>
{
public ScaleFilterType FilterType { get; set; } = ScaleFilterType.Prescale;
public UInt32 Scale { get; set; } = 1;
public UInt32 ChrRamBankSize { get; set; } = 0x1000;
public bool UseLargeSprites { get; set; } = false;
public bool SortByUsageFrequency { get; set; } = true;
public bool GroupBlankTiles { get; set; } = true;
public bool IgnoreOverscan { get; set; } = false;
public HdPackBuilderOptions ToInterop(string saveFolder)
{
return new HdPackBuilderOptions() {
SaveFolder = saveFolder,
FilterType = FilterType,
Scale = Scale,
ChrRamBankSize = ChrRamBankSize,
UseLargeSprites = UseLargeSprites,
SortByUsageFrequency = SortByUsageFrequency,
GroupBlankTiles = GroupBlankTiles,
IgnoreOverscan = IgnoreOverscan,
};
}
}
public enum ScaleFilterType
{
xBRZ = 0,
HQX = 1,
Scale2x = 2,
_2xSai = 3,
Super2xSai = 4,
SuperEagle = 5,
Prescale = 6,
}
public struct HdPackBuilderOptions
{
[MarshalAs(UnmanagedType.LPUTF8Str)] public string SaveFolder;
public ScaleFilterType FilterType;
public UInt32 Scale;
public UInt32 ChrRamBankSize;
[MarshalAs(UnmanagedType.I1)] public bool UseLargeSprites;
[MarshalAs(UnmanagedType.I1)] public bool SortByUsageFrequency;
[MarshalAs(UnmanagedType.I1)] public bool GroupBlankTiles;
[MarshalAs(UnmanagedType.I1)] public bool IgnoreOverscan;
}
}

View file

@ -129,6 +129,8 @@ namespace Mesen.Config.Shortcuts
VsInsertCoin2,
VsInsertCoin3,
VsInsertCoin4,
StartRecordHdPack,
StopRecordHdPack,
LastValidValue,
[Obsolete] LoadRandomGame,

View file

@ -79,6 +79,8 @@ namespace Mesen.Interop
[DllImport(DllPath)] public static extern double GetAspectRatio();
[DllImport(DllPath)] public static extern FrameInfo GetBaseScreenSize();
[DllImport(DllPath)] public static extern Int32 GetGameMemorySize(MemoryType type);
[DllImport(DllPath)] public static extern void SetRendererSize(UInt32 width, UInt32 height);
[DllImport(DllPath)] public static extern void ExecuteShortcut(ExecuteShortcutParams p);
@ -272,6 +274,7 @@ namespace Mesen.Interop
{
public EmulatorShortcut Shortcut;
public UInt32 Param;
public IntPtr ParamPtr;
};
public enum AudioPlayerAction
@ -349,4 +352,5 @@ namespace Mesen.Interop
[MarshalAs(UnmanagedType.I1)] public bool IsRamCode;
[MarshalAs(UnmanagedType.I1)] public bool IsAbsoluteAddress;
}
}

View file

@ -703,7 +703,30 @@
<Control ID="btnOK">OK</Control>
<Control ID="btnCancel">Cancel</Control>
</Form>
<Form ID="HdPackBuilderWindow">
<Control ID="wndTitle">HD Pack Builder</Control>
<Control ID="grpOptions">Recording Options</Control>
<Control ID="lblBankSize">CHR Bank Size:</Control>
<Control ID="lblScale">Scale/Filter:</Control>
<Control ID="chkSortByUsageFrequency">Sort pages by usage frequency</Control>
<Control ID="chkUseLargeSprites">Use 8x16 sprite display mode</Control>
<Control ID="chkGroupBlankTiles">Group blank tiles</Control>
<Control ID="chkIgnoreOverscan">Ignore tiles at the edges of the screen (overscan)</Control>
<Message ID="lblScaleHelp">Selects the scale and video filter to use when generating the PNG files&#xA;for the HD Pack. Use the "Prescale" filters to generate the tiles&#xA;at a larger scale without applying any transformation to the pixels.</Message>
<Message ID="lblBankSizeHelp">This option is only available for CHR RAM games. CHR RAM games have no&#xA;fixed "banks" - they are dynamically created by the game's code.&#xA;This option alters the HD Pack Builder's behavior when grouping the tiles into &#xA;the PNG files - a smaller bank size will usually result in less PNG &#xA;files (but depending on the game's code, larger values may produce &#xA;better results).</Message>
<Message ID="lblSortByUsageFrequencyHelp">When this option is enabled, the tiles in PNG files are sorted by the&#xA;frequency at which they are shown on the screen while recording (more &#xA;common palettes will be grouped together in the first PNG for a specific bank &#xA;number. If this option is unchecked, the PNGs will be sorted by palette - &#xA;each PNG will only contain up to 4 different colors in this case.</Message>
<Message ID="lblGroupBlankTilesHelp">This option groups all the blank tiles sequentially into the same PNG&#xA;files - this helps reduce the number of PNG files produced by removing &#xA;almost-empty PNG files containing only blank tiles.</Message>
<Message ID="lblUseLargeSpritesHelp">When enabled, this option will alter the display order of CHR banks&#xA;that contain only sprites to make the sprites easier to edit in the PNG file.</Message>
<Message ID="lblIgnoreOverscanHelp">When enabled, this will make the builder ignore any pixels in the overscan area.&#xA;This is useful in games that contain glitches on the outer edge of the screen.&#xA;Incorrect palette combinations due to these glitches will be ignored and won't be shown in the PNG files.</Message>
<Control ID="btnStartRecording">Start Recording</Control>
<Control ID="btnStopRecording">Stop Recording</Control>
<Control ID="btnOpenFolder">Open Save Folder</Control>
<Control ID="lblSaveTo">Save to:</Control>
</Form>
<Form ID="InputBarcodeWindow">
<Control ID="wndTitle">Input barcode...</Control>
<Control ID="lblBarcode">Barcode:</Control>

View file

@ -374,6 +374,9 @@
<Compile Update="Windows\GameConfigWindow.axaml.cs">
<DependentUpon>GameConfigWindow.axaml</DependentUpon>
</Compile>
<Compile Update="Windows\HdPackBuilderWindow.axaml.cs">
<DependentUpon>HdPackBuilderWindow.axaml</DependentUpon>
</Compile>
<Compile Update="Windows\UpdatePromptWindow.axaml.cs">
<DependentUpon>UpdatePromptWindow.axaml</DependentUpon>
</Compile>

View file

@ -0,0 +1,186 @@
using Avalonia.Threading;
using Mesen.Config;
using Mesen.Config.Shortcuts;
using Mesen.Interop;
using Mesen.Localization;
using Mesen.Utilities;
using ReactiveUI;
using ReactiveUI.Fody.Helpers;
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Reactive.Linq;
using System.Runtime.InteropServices;
using System.Text.RegularExpressions;
using System.Threading.Tasks;
namespace Mesen.ViewModels
{
public class HdPackBuilderViewModel : DisposableViewModel
{
[Reactive] public string SaveFolder { get; set; }
[Reactive] public bool IsRecording { get; set; }
[Reactive] public bool IsBankSizeVisible { get; set; }
[Reactive] public bool IsOpenFolderEnabled { get; set; }
[Reactive] public HdPackBuilderConfig Config { get; set; }
[Reactive] public FilterInfo? SelectedFilter { get; set; }
[Reactive] public BankSizeInfo SelectedBankSize { get; set; }
[Reactive] public FilterInfo[] Filters { get; private set; } = Array.Empty<FilterInfo>();
public BankSizeInfo[] BankSizes { get; } = {
new BankSizeInfo() { Name = "1 KB", BankSize = 0x400 },
new BankSizeInfo() { Name = "2 KB", BankSize = 0x800 },
new BankSizeInfo() { Name = "4 KB", BankSize = 0x1000 },
};
public HdPackBuilderViewModel()
{
Config = ConfigManager.Config.HdPackBuilder;
SaveFolder = Path.Join(ConfigManager.HdPackFolder, EmuApi.GetRomInfo().GetRomName());
UpdateFilterDropdown();
SelectedFilter = Filters.Where(x => x.FilterType == Config.FilterType && x.Scale == Config.Scale).FirstOrDefault() ?? Filters[0];
SelectedBankSize = BankSizes.Where(x => x.BankSize == Config.ChrRamBankSize).FirstOrDefault() ?? BankSizes[0];
IsBankSizeVisible = EmuApi.GetGameMemorySize(MemoryType.NesChrRam) > 0;
AddDisposable(this.WhenAnyValue(x => x.SelectedFilter).Subscribe(x => {
if(x != null) {
Config.FilterType = x.FilterType;
Config.Scale = x.Scale;
}
}));
AddDisposable(this.WhenAnyValue(x => x.SelectedBankSize).Subscribe(x => {
Config.ChrRamBankSize = x.BankSize;
}));
AddDisposable(this.WhenAnyValue(x => x.SaveFolder).Subscribe(x => {
IsOpenFolderEnabled = File.Exists(SaveFolder);
UpdateFilterDropdown();
}));
}
private void UpdateFilterDropdown()
{
string hdDefFile = Path.Combine(SaveFolder, "hires.txt");
FilterInfo? selectedFilter = SelectedFilter;
if(File.Exists(hdDefFile)) {
string fileContent = File.ReadAllText(hdDefFile);
Match match = Regex.Match(fileContent, "<scale>(\\d*)");
if(match.Success) {
int scale;
if(Int32.TryParse(match.Groups[1].ToString(), out scale)) {
Filters = _allFilters.Where(x => x.Scale == scale).ToArray();
SelectedFilter = Filters.Contains(selectedFilter) ? selectedFilter : Filters[0];
return;
}
}
}
Filters = _allFilters;
SelectedFilter = selectedFilter;
}
public void StartRecording()
{
IsRecording = true;
Task.Run(() => {
HdPackBuilderOptions options = Config.ToInterop(SaveFolder);
if(!IsBankSizeVisible) {
options.ChrRamBankSize = 0x1000;
}
IntPtr optionsPtr = Marshal.AllocHGlobal(Marshal.SizeOf(options));
try {
Marshal.StructureToPtr(options, optionsPtr, false);
EmuApi.ExecuteShortcut(new ExecuteShortcutParams() {
Shortcut = EmulatorShortcut.StartRecordHdPack,
ParamPtr = optionsPtr
});
} finally {
Marshal.FreeHGlobal(optionsPtr);
}
});
}
public void StopRecording()
{
IsRecording = false;
Task.Run(() => {
EmuApi.ExecuteShortcut(new ExecuteShortcutParams() { Shortcut = EmulatorShortcut.StopRecordHdPack });
Dispatcher.UIThread.Post(() => {
IsOpenFolderEnabled = true;
UpdateFilterDropdown();
});
});
}
public void OpenFolder()
{
if(Directory.Exists(SaveFolder)) {
System.Diagnostics.Process.Start(new System.Diagnostics.ProcessStartInfo() {
FileName = SaveFolder + Path.DirectorySeparatorChar,
UseShellExecute = true,
Verb = "open"
});
}
}
public class FilterInfo
{
public string Name { get; set; } = "";
public ScaleFilterType FilterType { get; set; }
public UInt32 Scale { get; set; }
public override string ToString()
{
return Name;
}
}
public class BankSizeInfo
{
public string Name { get; set; } = "";
public UInt32 BankSize { get; set; }
public override string ToString()
{
return Name;
}
}
static private FilterInfo[] _allFilters = {
new FilterInfo() { Name = ResourceHelper.GetEnumText(VideoFilterType.None) + " (1x)", FilterType = ScaleFilterType.Prescale, Scale = 1 },
new FilterInfo() { Name = ResourceHelper.GetEnumText(VideoFilterType.Prescale2x), FilterType = ScaleFilterType.Prescale, Scale = 2 },
new FilterInfo() { Name = ResourceHelper.GetEnumText(VideoFilterType.Prescale3x), FilterType = ScaleFilterType.Prescale, Scale = 3 },
new FilterInfo() { Name = ResourceHelper.GetEnumText(VideoFilterType.Prescale4x), FilterType = ScaleFilterType.Prescale, Scale = 4 },
new FilterInfo() { Name = ResourceHelper.GetEnumText(VideoFilterType.Prescale6x), FilterType = ScaleFilterType.Prescale, Scale = 6 },
new FilterInfo() { Name = ResourceHelper.GetEnumText(VideoFilterType.Prescale8x), FilterType = ScaleFilterType.Prescale, Scale = 8 },
new FilterInfo() { Name = ResourceHelper.GetEnumText(VideoFilterType.Prescale10x), FilterType = ScaleFilterType.Prescale, Scale = 10 },
new FilterInfo() { Name = ResourceHelper.GetEnumText(VideoFilterType.HQ2x), FilterType = ScaleFilterType.HQX, Scale = 2 },
new FilterInfo() { Name = ResourceHelper.GetEnumText(VideoFilterType.HQ3x), FilterType = ScaleFilterType.HQX, Scale = 3 },
new FilterInfo() { Name = ResourceHelper.GetEnumText(VideoFilterType.HQ4x), FilterType = ScaleFilterType.HQX, Scale = 4 },
new FilterInfo() { Name = ResourceHelper.GetEnumText(VideoFilterType.Scale2x), FilterType = ScaleFilterType.Scale2x, Scale = 2 },
new FilterInfo() { Name = ResourceHelper.GetEnumText(VideoFilterType.Scale3x), FilterType = ScaleFilterType.Scale2x, Scale = 3 },
new FilterInfo() { Name = ResourceHelper.GetEnumText(VideoFilterType.Scale4x), FilterType = ScaleFilterType.Scale2x, Scale = 4 },
new FilterInfo() { Name = ResourceHelper.GetEnumText(VideoFilterType.Super2xSai), FilterType = ScaleFilterType.Super2xSai, Scale = 2 },
new FilterInfo() { Name = ResourceHelper.GetEnumText(VideoFilterType.SuperEagle), FilterType = ScaleFilterType.SuperEagle, Scale = 2 },
new FilterInfo() { Name = ResourceHelper.GetEnumText(VideoFilterType._2xSai), FilterType = ScaleFilterType._2xSai, Scale = 2 },
new FilterInfo() { Name = ResourceHelper.GetEnumText(VideoFilterType.xBRZ2x), FilterType = ScaleFilterType.xBRZ, Scale = 2 },
new FilterInfo() { Name = ResourceHelper.GetEnumText(VideoFilterType.xBRZ3x), FilterType = ScaleFilterType.xBRZ, Scale = 3 },
new FilterInfo() { Name = ResourceHelper.GetEnumText(VideoFilterType.xBRZ4x), FilterType = ScaleFilterType.xBRZ, Scale = 4 },
new FilterInfo() { Name = ResourceHelper.GetEnumText(VideoFilterType.xBRZ5x), FilterType = ScaleFilterType.xBRZ, Scale = 5 },
new FilterInfo() { Name = ResourceHelper.GetEnumText(VideoFilterType.xBRZ6x), FilterType = ScaleFilterType.xBRZ, Scale = 6 },
};
}
}

View file

@ -19,6 +19,7 @@ using System.IO.Compression;
using System.Linq;
using System.Reactive;
using System.Reactive.Linq;
using System.Runtime.InteropServices;
using System.Text;
using System.Text.RegularExpressions;
using System.Threading.Tasks;
@ -600,8 +601,9 @@ namespace Mesen.ViewModels
},
new MainMenuAction() {
ActionType = ActionType.HdPackBuilder,
IsEnabled = () => false,
OnClick = () => { } //TODO
OnClick = () => {
ApplicationHelper.GetOrCreateUniqueWindow(wnd, () => new HdPackBuilderWindow());
}
}
}
},

View file

@ -0,0 +1,62 @@
<Window
xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:m="clr-namespace:Mesen"
xmlns:c="using:Mesen.Controls"
xmlns:i="using:Mesen.Interop"
xmlns:cfg="using:Mesen.Config"
xmlns:vm="using:Mesen.ViewModels"
xmlns:l="using:Mesen.Localization"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
mc:Ignorable="d" d:DesignWidth="450" d:DesignHeight="230"
x:Class="Mesen.Windows.HdPackBuilderWindow"
Width="450" Height="230"
CanResize="False"
x:DataType="vm:HdPackBuilderViewModel"
Title="{l:Translate wndTitle}"
Icon="/Assets/HdPack.png"
>
<DockPanel Margin="5">
<Grid DockPanel.Dock="Bottom" ColumnDefinitions="Auto,*,Auto" RowDefinitions="Auto">
<c:ButtonWithIcon Icon="Assets/Folder.png" Command="{Binding OpenFolder}" Text="{l:Translate btnOpenFolder}" IsEnabled="{CompiledBinding IsOpenFolderEnabled}" />
<StackPanel Orientation="Horizontal" HorizontalAlignment="Right" Grid.Column="2">
<c:ButtonWithIcon Icon="Assets/Record.png" Command="{Binding StartRecording}" Text="{l:Translate btnStartRecording}" IsVisible="{CompiledBinding !IsRecording}" />
<c:ButtonWithIcon Icon="Assets/MediaStop.png" Command="{Binding StopRecording}" Text="{l:Translate btnStopRecording}" IsVisible="{CompiledBinding IsRecording}" />
</StackPanel>
</Grid>
<Grid ColumnDefinitions="Auto,1*" RowDefinitions="Auto,Auto,Auto,Auto,Auto">
<TextBlock Text="{l:Translate lblSaveTo}" />
<c:PathSelector Grid.Column="1" Path="{CompiledBinding SaveFolder}" Editable="True" />
<c:OptionSection Grid.Row="1" Grid.ColumnSpan="2" Header="{l:Translate grpOptions}">
<Grid ColumnDefinitions="Auto,150,Auto" RowDefinitions="Auto,Auto">
<TextBlock VerticalAlignment="Center" Text="{l:Translate lblScale}" />
<ComboBox Grid.Column="1" HorizontalAlignment="Stretch" Items="{CompiledBinding Filters}" SelectedItem="{CompiledBinding SelectedFilter}" />
<Image Grid.Column="2" Stretch="None" Margin="5 0" Source="/Assets/Help.png" ToolTip.Tip="{l:Translate lblScaleHelp}" ToolTip.Placement="Right" ToolTip.ShowDelay="100" />
<TextBlock Grid.Row="1" VerticalAlignment="Center" Text="{l:Translate lblBankSize}" IsVisible="{CompiledBinding IsBankSizeVisible}" />
<ComboBox Grid.Column="1" Grid.Row="1" HorizontalAlignment="Stretch" Items="{CompiledBinding BankSizes}" SelectedItem="{CompiledBinding SelectedBankSize}" IsVisible="{CompiledBinding IsBankSizeVisible}" />
<Image Grid.Column="2" Grid.Row="1" Stretch="None" Margin="5 0" Source="/Assets/Help.png" ToolTip.Tip="{l:Translate lblBankSizeHelp}" ToolTip.Placement="Right" ToolTip.ShowDelay="100" IsVisible="{CompiledBinding IsBankSizeVisible}" />
</Grid>
<WrapPanel>
<CheckBox Content="{l:Translate chkGroupBlankTiles}" IsChecked="{CompiledBinding Config.GroupBlankTiles}" />
<Image Stretch="None" Margin="5 0" Source="/Assets/Help.png" ToolTip.Tip="{l:Translate lblGroupBlankTilesHelp}" ToolTip.Placement="Right" ToolTip.ShowDelay="100" />
</WrapPanel>
<WrapPanel>
<CheckBox Content="{l:Translate chkSortByUsageFrequency}" IsChecked="{CompiledBinding Config.SortByUsageFrequency}" />
<Image Stretch="None" Margin="5 0" Source="/Assets/Help.png" ToolTip.Tip="{l:Translate lblSortByUsageFrequencyHelp}" ToolTip.Placement="Right" ToolTip.ShowDelay="100" />
</WrapPanel>
<WrapPanel>
<CheckBox Content="{l:Translate chkUseLargeSprites}" IsChecked="{CompiledBinding Config.UseLargeSprites}" />
<Image Stretch="None" Margin="5 0" Source="/Assets/Help.png" ToolTip.Tip="{l:Translate lblUseLargeSpritesHelp}" ToolTip.Placement="Right" ToolTip.ShowDelay="100" />
</WrapPanel>
<WrapPanel>
<CheckBox Content="{l:Translate chkIgnoreOverscan}" IsChecked="{CompiledBinding Config.IgnoreOverscan}" />
<Image Stretch="None" Margin="5 0" Source="/Assets/Help.png" ToolTip.Tip="{l:Translate lblIgnoreOverscanHelp}" ToolTip.Placement="Right" ToolTip.ShowDelay="100" />
</WrapPanel>
</c:OptionSection>
</Grid>
</DockPanel>
</Window>

View file

@ -0,0 +1,39 @@
using Avalonia;
using Avalonia.Controls;
using Avalonia.Markup.Xaml;
using Mesen.ViewModels;
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Runtime.InteropServices;
using System.Threading.Tasks;
namespace Mesen.Windows
{
public class HdPackBuilderWindow : Window
{
private HdPackBuilderViewModel _model;
public HdPackBuilderWindow()
{
_model = new HdPackBuilderViewModel();
DataContext = _model;
InitializeComponent();
#if DEBUG
this.AttachDevTools();
#endif
}
protected override void OnClosing(CancelEventArgs e)
{
base.OnClosing(e);
_model.Dispose();
}
private void InitializeComponent()
{
AvaloniaXamlLoader.Load(this);
}
}
}

View file

@ -277,6 +277,10 @@ namespace Mesen.Windows
}, TimeSpan.FromMilliseconds(50));
});
}
Dispatcher.UIThread.Post(() => {
ApplicationHelper.GetExistingWindow<HdPackBuilderWindow>()?.Close();
});
break;
case ConsoleNotificationType.DebuggerResumed:
@ -321,6 +325,12 @@ namespace Mesen.Windows
});
tcs.Task.Wait();
break;
case ConsoleNotificationType.BeforeGameLoad:
Dispatcher.UIThread.Post(() => {
ApplicationHelper.GetExistingWindow<HdPackBuilderWindow>()?.Close();
});
break;
}
}