PPU
This commit is contained in:
parent
608a2b7578
commit
07d044c47f
|
@ -8,9 +8,9 @@
|
|||
#define NESEMULATOR_MAPPER_H
|
||||
|
||||
typedef struct mapper {
|
||||
address prg_rom_start_addr;
|
||||
byte *(*mem_read)(address);
|
||||
|
||||
void (*post_prg_load)(unsigned int);
|
||||
byte *(*ppu_read)(address);
|
||||
} Mapper;
|
||||
|
||||
enum MapperType {
|
||||
|
|
|
@ -5,6 +5,7 @@
|
|||
#include <stdbool.h>
|
||||
#include <stddef.h>
|
||||
#include "types.h"
|
||||
#include "../ppu/memory.h"
|
||||
|
||||
#ifndef NESEMULATOR_PPU_H
|
||||
#define NESEMULATOR_PPU_H
|
||||
|
@ -52,7 +53,17 @@
|
|||
|
||||
#define PPU_MASK_NONE 0xff
|
||||
|
||||
#define PATTERN_TABLE_SIZE 0x1000
|
||||
|
||||
typedef struct ppu_memory {
|
||||
byte *nametable_0;
|
||||
byte *nametable_1;
|
||||
byte *palette;
|
||||
} PPUMemory;
|
||||
|
||||
typedef struct ppu {
|
||||
PPUMemory memory;
|
||||
|
||||
byte *registers;
|
||||
byte *oam_dma_register;
|
||||
byte vram[PPU_VRAM_SIZE];
|
||||
|
@ -63,8 +74,9 @@ typedef struct ppu {
|
|||
byte x;
|
||||
bool w;
|
||||
|
||||
bool fetching;
|
||||
address ppu_address;
|
||||
|
||||
PPUTileFetch tile_fetch;
|
||||
unsigned long frame;
|
||||
unsigned int scanline;
|
||||
unsigned int cycle;
|
||||
|
@ -79,6 +91,7 @@ PPU *ppu_get_state();
|
|||
* @param ppu
|
||||
*/
|
||||
void ppu_init(byte *registers_ram, byte *oam_dma_register);
|
||||
void ppu_uninit();
|
||||
|
||||
/**
|
||||
* Cycles the PPU.
|
||||
|
|
|
@ -15,11 +15,17 @@
|
|||
#define ROM_TRAINER_SIZE 512
|
||||
|
||||
typedef struct {
|
||||
byte header[ROM_HEADER_SIZE];
|
||||
bool nametable_mirrored;
|
||||
|
||||
byte *prg_rom;
|
||||
int prg_rom_size;
|
||||
|
||||
byte *chr_rom;
|
||||
void *header;
|
||||
} Rom;
|
||||
|
||||
Rom *rom_get();
|
||||
|
||||
/**
|
||||
* Loads a ROM from a specified file path.
|
||||
*
|
||||
|
@ -28,4 +34,6 @@ typedef struct {
|
|||
*/
|
||||
bool rom_load(char *path);
|
||||
|
||||
void rom_unload();
|
||||
|
||||
#endif //NESEMULATOR_ROM_H
|
3
main.c
3
main.c
|
@ -27,7 +27,7 @@ int main() {
|
|||
log_set_level(LOG_INFO);
|
||||
|
||||
system_init();
|
||||
char *rom_path = "../test_roms/smb.nes";
|
||||
char *rom_path = "../test_roms/dk_japan.nes";
|
||||
|
||||
if (!rom_load(rom_path)) {
|
||||
system_uninit();
|
||||
|
@ -51,6 +51,7 @@ int main() {
|
|||
}
|
||||
|
||||
system_uninit();
|
||||
rom_unload();
|
||||
gui_uninit();
|
||||
|
||||
//// start_debugger();
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
set(SOURCE simple_mapper.c mappers.c)
|
||||
set(SOURCE nrom.c mappers.c)
|
||||
|
||||
add_library(nes_mappers ${SOURCE})
|
||||
|
||||
|
|
|
@ -6,7 +6,7 @@
|
|||
#include <stdlib.h>
|
||||
|
||||
#include "../include/mapper.h"
|
||||
#include "simple_mapper.c"
|
||||
#include "nrom.c"
|
||||
|
||||
Mapper get_mapper(enum MapperType type) {
|
||||
switch (type) {
|
||||
|
|
|
@ -0,0 +1,38 @@
|
|||
#include "../include/mapper.h"
|
||||
#include "../include/rom.h"
|
||||
#include "../cpu/memory.h"
|
||||
|
||||
#define SIMPLE_MAPPER_PRG_START_ADDR 0x8000
|
||||
#define PRG_BANK_SIZE 0x4000 // 16Kb
|
||||
|
||||
byte *nrom_mem_read(address addr) {
|
||||
if (addr >= SIMPLE_MAPPER_PRG_START_ADDR) {
|
||||
Rom *rom = rom_get();
|
||||
address relative_addr = addr - SIMPLE_MAPPER_PRG_START_ADDR;
|
||||
|
||||
if (addr < PRG_BANK_SIZE || rom->prg_rom_size > PRG_BANK_SIZE) {
|
||||
return &rom->prg_rom[relative_addr];
|
||||
}
|
||||
|
||||
// The second bank is mirrored
|
||||
return &rom->prg_rom[relative_addr - PRG_BANK_SIZE];
|
||||
}
|
||||
|
||||
return NULL;
|
||||
}
|
||||
|
||||
byte *nrom_ppu_read(address addr) {
|
||||
if (addr < 0x2000) {
|
||||
Rom *rom = rom_get();
|
||||
return &rom->chr_rom[addr];
|
||||
}
|
||||
|
||||
return NULL;
|
||||
}
|
||||
|
||||
Mapper get_simple_mapper() {
|
||||
Mapper mapper;
|
||||
mapper.mem_read = &nrom_mem_read;
|
||||
mapper.ppu_read = &nrom_ppu_read;
|
||||
return mapper;
|
||||
}
|
|
@ -1,26 +0,0 @@
|
|||
#include "../include/mapper.h"
|
||||
#include "../include/rom.h"
|
||||
#include "../cpu/memory.h"
|
||||
#include <string.h>
|
||||
|
||||
#define SIMPLE_MAPPER_PRG_START_ADDR 0x8000
|
||||
#define PRG_PART_SIZE 0x4000 // 16Kb
|
||||
|
||||
void post_prg_load(unsigned int prg_size) {
|
||||
if (prg_size == 2) {
|
||||
// The whole space is occupied, nothing to do
|
||||
return;
|
||||
}
|
||||
|
||||
// We need to mirror the data in the upper ram
|
||||
byte *source = mem_get_ptr(SIMPLE_MAPPER_PRG_START_ADDR);
|
||||
byte *destination = mem_get_ptr(SIMPLE_MAPPER_PRG_START_ADDR + PRG_PART_SIZE);
|
||||
memcpy(destination, source, PRG_PART_SIZE);
|
||||
}
|
||||
|
||||
Mapper get_simple_mapper() {
|
||||
Mapper mapper;
|
||||
mapper.prg_rom_start_addr = SIMPLE_MAPPER_PRG_START_ADDR;
|
||||
mapper.post_prg_load = &post_prg_load;
|
||||
return mapper;
|
||||
}
|
|
@ -1,5 +1,7 @@
|
|||
set(HEADERS pattern_table.h ppu.h palette.h)
|
||||
set(SOURCE pattern_table.c ppu.c palette.c)
|
||||
set(HEADERS pattern_table.h ppu.h palette.h
|
||||
memory.h)
|
||||
set(SOURCE pattern_table.c ppu.c palette.c
|
||||
memory.c)
|
||||
|
||||
add_library(nes_ppu ${SOURCE} ${HEADERS})
|
||||
|
||||
|
|
|
@ -0,0 +1,48 @@
|
|||
//
|
||||
// Created by william on 5/17/24.
|
||||
//
|
||||
|
||||
#include <assert.h>
|
||||
#include "memory.h"
|
||||
|
||||
void ppu_vram_fetch(PPUVramFetch *fetch, address addr) {
|
||||
assert(addr < VRAM_SIZE);
|
||||
|
||||
if (fetch->finished) {
|
||||
fetch->finished = false;
|
||||
return;
|
||||
}
|
||||
|
||||
fetch->data = *fetch->vram[addr];
|
||||
fetch->finished = true;
|
||||
}
|
||||
|
||||
void ppu_tile_fetch(PPUTileFetch *fetch, address addr) {
|
||||
if (fetch->fetch_cycle >= 8) {
|
||||
fetch->fetch_cycle = 0;
|
||||
}
|
||||
|
||||
if (fetch->fetch_cycle % 2 == 0) {
|
||||
// First cycle of a memory fetch
|
||||
ppu_vram_fetch(&fetch->vram_fetch, addr + (fetch->fetch_cycle / 2));
|
||||
} else {
|
||||
// Second cycle of a fetch, the data should be available
|
||||
byte data = fetch->vram_fetch.data;
|
||||
switch (fetch->fetch_cycle) {
|
||||
case 1:
|
||||
fetch->nametable = data;
|
||||
break;
|
||||
case 3:
|
||||
fetch->attribute_table = data;
|
||||
break;
|
||||
case 5:
|
||||
fetch->pattern_table_tile_low = data;
|
||||
break;
|
||||
case 7:
|
||||
fetch->pattern_table_tile_high = data;
|
||||
break;
|
||||
default:
|
||||
assert(false);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,28 @@
|
|||
//
|
||||
// Created by william on 5/17/24.
|
||||
//
|
||||
|
||||
#include <stdbool.h>
|
||||
#include "../include/types.h"
|
||||
|
||||
#ifndef NES_EMULATOR_MEMORY_H
|
||||
#define NES_EMULATOR_MEMORY_H
|
||||
|
||||
typedef struct ppu_vram_fetch {
|
||||
vram *vram;
|
||||
byte data;
|
||||
bool finished;
|
||||
} PPUVramFetch;
|
||||
|
||||
typedef struct ppu_tile_fetch {
|
||||
byte nametable;
|
||||
byte attribute_table;
|
||||
byte pattern_table_tile_low;
|
||||
byte pattern_table_tile_high;
|
||||
} PPUTileFetch;
|
||||
|
||||
void ppu_vram_fetch(PPUVramFetch *fetch, address addr);
|
||||
|
||||
void ppu_tile_fetch(PPUTileFetch *fetch, address addr);
|
||||
|
||||
#endif //NES_EMULATOR_MEMORY_H
|
118
ppu/ppu.c
118
ppu/ppu.c
|
@ -12,12 +12,13 @@
|
|||
// 8. Implement OAMDMA (and OAMDATA I guess, I implemented one on top of the other)
|
||||
// 9. Now you should have sprite data to render. Implement the logic for copying from primary OAM to scanline OAM. I'm doing it all as one step (not smearing it over up to 256 cycles like the actual hardware). Skip the confusing sprite overflow junk.
|
||||
// 10. This is where I'm stuck. I think I need to read the "sprites" section of https://wiki.nesdev.com/w/index.php/PPU_rendering very carefully.
|
||||
//
|
||||
|
||||
#include <assert.h>
|
||||
#include <string.h>
|
||||
#include "ppu.h"
|
||||
#include "../include/ppu.h"
|
||||
#include "../cpu/cpu.h"
|
||||
#include "../include/rom.h"
|
||||
|
||||
#define PPU_VISIBLE_FRAME_END 240
|
||||
#define PPU_POST_RENDER_LINE_START PPU_VISIBLE_FRAME_END
|
||||
|
@ -29,22 +30,11 @@
|
|||
PPU ppu_state;
|
||||
|
||||
void ppu_init(byte *registers_ram, byte *oam_dma_register) {
|
||||
ppu_state.registers = registers_ram;
|
||||
ppu_state.registers[PPU_REGISTER_CTRL] = 0x00;
|
||||
ppu_state.registers[PPU_REGISTER_MASK] = 0x00;
|
||||
ppu_state.registers[PPU_REGISTER_STATUS] = 0x00;
|
||||
ppu_state.registers[PPU_REGISTER_OAM_ADDR] = 0x00;
|
||||
ppu_state.registers[PPU_REGISTER_OAM_DATA] = 0x00;
|
||||
ppu_state.registers[PPU_REGISTER_SCROLL] = 0x00;
|
||||
ppu_state.registers[PPU_REGISTER_ADDR] = 0x00;
|
||||
ppu_state.registers[PPU_REGISTER_DATA] = 0x00;
|
||||
ppu_state.oam_dma_register = oam_dma_register;
|
||||
ppu_state.odd_frame = false;
|
||||
ppu_state.fetching = false;
|
||||
memset(&ppu_state, 0, sizeof(PPU));
|
||||
|
||||
ppu_state.frame = 0;
|
||||
ppu_state.scanline = 0;
|
||||
ppu_state.cycle = 0;
|
||||
ppu_state.oam_dma_register = oam_dma_register;
|
||||
ppu_state.registers = registers_ram;
|
||||
memset(&ppu_state.registers, 0, 8);
|
||||
}
|
||||
|
||||
PPU *ppu_get_state() {
|
||||
|
@ -68,29 +58,28 @@ void ppu_trigger_vbl_nmi() {
|
|||
cpu_trigger_nmi();
|
||||
}
|
||||
|
||||
typedef struct {
|
||||
byte nametable;
|
||||
byte attribute_table;
|
||||
byte pattern_table_tile_low;
|
||||
byte pattern_table_tile_high;
|
||||
|
||||
byte fetch_tick;
|
||||
} Tile;
|
||||
Tile tile;
|
||||
|
||||
void ppu_visible_frame(unsigned int cycle) {
|
||||
if (cycle == 0) {
|
||||
// Idle...
|
||||
} else if (cycle <= 256) {
|
||||
switch (tile.fetch_tick) {
|
||||
|
||||
byte tile_fetch_cycle = (cycle - 1) % 8;
|
||||
switch (tile_fetch_cycle) {
|
||||
case 1:
|
||||
ppu_state.tile_fetch.nametable = ppu_read(ppu_state.ppu_address);
|
||||
break;
|
||||
case 3:
|
||||
ppu_state.tile_fetch.attribute_table = ppu_read(ppu_state.ppu_address);
|
||||
break;
|
||||
case 5:
|
||||
ppu_state.tile_fetch.pattern_table_tile_low = ppu_read(ppu_state.ppu_address);
|
||||
break;
|
||||
case 7:
|
||||
ppu_state.tile_fetch.pattern_table_tile_high = ppu_read(ppu_state.ppu_address);
|
||||
ppu_state.ppu_address++;
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
||||
if (tile.nametable == 0) {
|
||||
|
||||
}
|
||||
|
||||
tile.fetch_tick++;
|
||||
} else if (cycle <= 320) {
|
||||
// OAMADDR is cleared on sprite loading for pre-render and visible lines
|
||||
ppu_write_reg(PPU_REGISTER_OAM_ADDR, 0);
|
||||
|
@ -168,8 +157,8 @@ byte ppu_read_reg(byte reg) {
|
|||
// Access to VRAM memory is slow, so reading it a first time generally return the memory at the previous address.
|
||||
// So we get the data first, then update the register.
|
||||
byte data = ppu_state.registers[reg];
|
||||
ppu_state.registers[reg] = ppu_state.vram[ppu_state.v];
|
||||
if (ppu_state.v > 0x3eff) {
|
||||
ppu_state.registers[reg] = ppu_state.vram[ppu_state.ppu_address];
|
||||
if (ppu_state.ppu_address > 0x3eff) {
|
||||
// But the palette data is returned immediately
|
||||
data = ppu_state.registers[reg];
|
||||
}
|
||||
|
@ -180,9 +169,9 @@ byte ppu_read_reg(byte reg) {
|
|||
increment = 32;
|
||||
}
|
||||
|
||||
ppu_state.v += increment;
|
||||
if (ppu_state.v >= PPU_VRAM_SIZE) {
|
||||
ppu_state.v -= PPU_VRAM_SIZE;
|
||||
ppu_state.ppu_address += increment;
|
||||
if (ppu_state.ppu_address >= PPU_VRAM_SIZE) {
|
||||
ppu_state.ppu_address -= PPU_VRAM_SIZE;
|
||||
}
|
||||
|
||||
return data;
|
||||
|
@ -217,7 +206,7 @@ void ppu_write_reg(byte reg, byte data) {
|
|||
}
|
||||
} else if (reg == PPU_REGISTER_ADDR) {
|
||||
ppu_state.w = !ppu_state.w;
|
||||
address addr = ppu_state.v;
|
||||
address addr = ppu_state.ppu_address;
|
||||
if (ppu_state.w) {
|
||||
addr &= 0xff & data;
|
||||
} else {
|
||||
|
@ -227,18 +216,18 @@ void ppu_write_reg(byte reg, byte data) {
|
|||
if (addr >= PPU_VRAM_SIZE) {
|
||||
addr -= PPU_VRAM_SIZE;
|
||||
}
|
||||
ppu_state.v = addr;
|
||||
ppu_state.ppu_address = addr;
|
||||
} else if (reg == PPU_REGISTER_DATA) {
|
||||
ppu_state.vram[ppu_state.v] = data;
|
||||
ppu_state.vram[ppu_state.ppu_address] = data;
|
||||
|
||||
byte increment = 1;
|
||||
if (ppu_read_flag(PPU_REGISTER_CTRL, PPU_CTRL_VRAM_ADDR_INCREMENT)) {
|
||||
increment = 32;
|
||||
}
|
||||
|
||||
ppu_state.v += increment;
|
||||
if (ppu_state.v >= PPU_VRAM_SIZE) {
|
||||
ppu_state.v -= PPU_VRAM_SIZE;
|
||||
ppu_state.ppu_address += increment;
|
||||
if (ppu_state.ppu_address >= PPU_VRAM_SIZE) {
|
||||
ppu_state.ppu_address -= PPU_VRAM_SIZE;
|
||||
}
|
||||
} else if (reg == PPU_REGISTER_OAM_DATA) {
|
||||
byte oam_addr = ppu_state.registers[PPU_REGISTER_OAM_ADDR];
|
||||
|
@ -251,5 +240,42 @@ void ppu_write_reg(byte reg, byte data) {
|
|||
byte ppu_read(address addr) {
|
||||
assert(addr < PPU_VRAM_SIZE);
|
||||
|
||||
return ppu_state.vram[addr];
|
||||
address relative_addr;
|
||||
|
||||
if (addr < 0x2000) {
|
||||
return *system_get_mapper()->ppu_read(addr);
|
||||
} else if (addr < 0x2400) {
|
||||
relative_addr = addr - 0x2000;
|
||||
return ppu_state.memory.nametable_0[relative_addr];
|
||||
} else if (addr < 0x2800) {
|
||||
relative_addr = addr - 0x2400;
|
||||
byte *nametable;
|
||||
|
||||
if (rom_get()->nametable_mirrored) {
|
||||
nametable = ppu_state.memory.nametable_1;
|
||||
} else {
|
||||
nametable = ppu_state.memory.nametable_0;
|
||||
}
|
||||
|
||||
return nametable[relative_addr];
|
||||
} else if (addr < 0x2c00) {
|
||||
relative_addr = addr - 0x2800;
|
||||
byte *nametable;
|
||||
|
||||
if (rom_get()->nametable_mirrored) {
|
||||
nametable = ppu_state.memory.nametable_0;
|
||||
} else {
|
||||
nametable = ppu_state.memory.nametable_1;
|
||||
}
|
||||
|
||||
return nametable[relative_addr];
|
||||
} else if (addr < 0x3000) {
|
||||
relative_addr = addr - 0x2c00;
|
||||
return ppu_state.memory.nametable_1[relative_addr];
|
||||
} else if (addr >= 0x3f00) {
|
||||
relative_addr = (addr - 0x3f00) % 0x20;
|
||||
return ppu_state.memory.palette[relative_addr];
|
||||
}
|
||||
|
||||
assert(false);
|
||||
}
|
26
rom/ines.c
26
rom/ines.c
|
@ -4,6 +4,7 @@
|
|||
|
||||
#include <stdbool.h>
|
||||
#include <stdio.h>
|
||||
#include <malloc.h>
|
||||
#include "log.h"
|
||||
#include "../include/rom.h"
|
||||
#include "../include/system.h"
|
||||
|
@ -62,7 +63,7 @@ bool rom_is_ines(const char header[16]) {
|
|||
return header[0] == 'N' && header[1] == 'E' && header[2] == 'S';
|
||||
}
|
||||
|
||||
INesHeader read_header(const char header_buf[16]) {
|
||||
INesHeader read_header(const byte *header_buf) {
|
||||
INesHeader header;
|
||||
|
||||
unsigned char flag6 = header_buf[6];
|
||||
|
@ -127,12 +128,13 @@ bool rom_ines_read_trainer(FILE *file, INesHeader *header) {
|
|||
return false;
|
||||
}
|
||||
|
||||
bool rom_ines_read_prg_rom(FILE *file, INesHeader *header) {
|
||||
bool rom_ines_read_prg_rom(FILE *file, INesHeader *header, Rom *rom) {
|
||||
unsigned int prg_rom_size = header->prg_rom_size * 16384;
|
||||
log_debug("Reading %d bytes PRG ROM", prg_rom_size);
|
||||
|
||||
byte *prg_rom_location = mem_get_ptr(system_get_mapper()->prg_rom_start_addr);
|
||||
if (fread(prg_rom_location, sizeof(byte), prg_rom_size, file) < prg_rom_size) {
|
||||
rom->prg_rom_size = prg_rom_size;
|
||||
rom->prg_rom = malloc(prg_rom_size);
|
||||
if (fread(rom->prg_rom, sizeof(byte), prg_rom_size, file) < prg_rom_size) {
|
||||
log_error("Failed to read PRG ROM");
|
||||
return false;
|
||||
}
|
||||
|
@ -142,7 +144,7 @@ bool rom_ines_read_prg_rom(FILE *file, INesHeader *header) {
|
|||
return true;
|
||||
}
|
||||
|
||||
bool rom_ines_read_chr_rom(FILE *file, INesHeader *header) {
|
||||
bool rom_ines_read_chr_rom(FILE *file, INesHeader *header, Rom *rom) {
|
||||
if (header->chr_rom_size <= 0) {
|
||||
log_debug("No CHR ROM to read");
|
||||
return true;
|
||||
|
@ -151,8 +153,8 @@ bool rom_ines_read_chr_rom(FILE *file, INesHeader *header) {
|
|||
unsigned int chr_rom_size = header->chr_rom_size * 8192;
|
||||
log_debug("Reading %d bytes CHR ROM", chr_rom_size);
|
||||
|
||||
byte *chr_rom_location = ppu_get_state()->vram;
|
||||
if (fread(chr_rom_location, sizeof(byte), chr_rom_size, file) < chr_rom_size) {
|
||||
rom->chr_rom = malloc(sizeof(byte) * chr_rom_size);
|
||||
if (fread(rom->chr_rom, sizeof(byte), chr_rom_size, file) < chr_rom_size) {
|
||||
log_error("Failed to read CHR ROM");
|
||||
return false;
|
||||
}
|
||||
|
@ -160,11 +162,11 @@ bool rom_ines_read_chr_rom(FILE *file, INesHeader *header) {
|
|||
return true;
|
||||
}
|
||||
|
||||
bool rom_ines_read(const char header_buf[ROM_HEADER_SIZE], FILE *file) {
|
||||
INesHeader header = read_header(header_buf);
|
||||
// system->rom_header = &header;
|
||||
bool rom_ines_read(Rom *rom, FILE *file) {
|
||||
INesHeader header = read_header(rom->header);
|
||||
rom->nametable_mirrored = header.flags.nametable_mirrored;
|
||||
|
||||
return rom_ines_read_trainer(file, &header) &&
|
||||
rom_ines_read_prg_rom(file, &header) &&
|
||||
rom_ines_read_chr_rom(file, &header);
|
||||
rom_ines_read_prg_rom(file, &header, rom) &&
|
||||
rom_ines_read_chr_rom(file, &header, rom);
|
||||
}
|
13
rom/rom.c
13
rom/rom.c
|
@ -8,6 +8,12 @@
|
|||
#include "ines.c"
|
||||
#include "../include/system.h"
|
||||
|
||||
Rom rom;
|
||||
|
||||
Rom *rom_get() {
|
||||
return &rom;
|
||||
}
|
||||
|
||||
bool rom_load(char *path) {
|
||||
FILE *file = fopen(path, "r");
|
||||
if (!file) {
|
||||
|
@ -28,7 +34,7 @@ bool rom_load(char *path) {
|
|||
}
|
||||
|
||||
log_info("Reading iNes 1.0 ROM at %s", path);
|
||||
rom_ines_read(header_buffer, file);
|
||||
rom_ines_read(&rom, file);
|
||||
|
||||
if (fclose(file) != 0) {
|
||||
log_error("Failed to close ROM file");
|
||||
|
@ -37,3 +43,8 @@ bool rom_load(char *path) {
|
|||
|
||||
return true;
|
||||
}
|
||||
|
||||
void rom_unload() {
|
||||
free(rom.prg_rom);
|
||||
free(rom.chr_rom);
|
||||
}
|
Loading…
Reference in New Issue