mirror of
https://github.com/daniel5151/ANESE.git
synced 2024-06-01 10:57:23 -04:00
add zelda heuristic + cleanup
didn't think i'd be able to pull it off, but Zelda seems to scroll fine now! All I needed to do was sniff the PPUADDR register, since after all, PPUSCROLL and PPUADDR both point to PPU t/v anyways... I also bunched-up the heauristics a bit better, hopefully making the code a bit nicer. before showing off my work, the big thing that needs to get done is scene detection. Yeah, heuristics are cool, and fun to implement, but I can do that later. What I _should_ focus on is getting scene- detection working, and afterwards, saving and loading, since that's the killer feature (imo) It's going to be tricky, but i've been reading-up on perceptual hashes, so that might be useful? Alternatively, I could sniff what games tend to write to the PPU during "scene transitions" and see if there is anything I can use to fingerprint scenes...
This commit is contained in:
parent
fc350a64ad
commit
cfdb31e6dc
|
@ -11,6 +11,7 @@ WideNESModule::WideNESModule(SharedState& gui)
|
|||
gui.nes._callbacks.cart_changed.add_cb(WideNESModule::cb_mapper_changed, this);
|
||||
gui.nes._ppu()._callbacks.frame_end.add_cb(WideNESModule::cb_ppu_frame_end, this);
|
||||
gui.nes._ppu()._callbacks.write_start.add_cb(WideNESModule::cb_ppu_write_start, this);
|
||||
gui.nes._ppu()._callbacks.write_end.add_cb(WideNESModule::cb_ppu_write_end, this);
|
||||
|
||||
/*------------------------------- SDL init -------------------------------*/
|
||||
|
||||
|
@ -167,6 +168,10 @@ void WideNESModule::cb_ppu_write_start(void* self, u16 addr, u8 val) {
|
|||
((WideNESModule*)self)->ppu_write_start_handler(addr, val);
|
||||
}
|
||||
|
||||
void WideNESModule::cb_ppu_write_end(void* self, u16 addr, u8 val) {
|
||||
((WideNESModule*)self)->ppu_write_end_handler(addr, val);
|
||||
}
|
||||
|
||||
void WideNESModule::cb_mapper_changed(void* self, Mapper* mapper) {
|
||||
if (mapper && mapper->mapper_number() == 4) {
|
||||
Mapper_004* mmc3 = static_cast<Mapper_004*>(mapper);
|
||||
|
@ -174,8 +179,8 @@ void WideNESModule::cb_mapper_changed(void* self, Mapper* mapper) {
|
|||
}
|
||||
}
|
||||
|
||||
void WideNESModule::cb_mmc3_irq(void* self, Mapper_004* mmc3, bool active) {
|
||||
((WideNESModule*)self)->mmc3_irq_handler(mmc3, active);
|
||||
void WideNESModule::cb_mmc3_irq(void* self, Mapper_004* mmc3, bool irq_enabled) {
|
||||
((WideNESModule*)self)->mmc3_irq_handler(mmc3, irq_enabled);
|
||||
}
|
||||
|
||||
void WideNESModule::cb_ppu_frame_end(void* self) {
|
||||
|
@ -187,6 +192,12 @@ void WideNESModule::cb_ppu_frame_end(void* self) {
|
|||
#include "nes/ppu/ppu.h"
|
||||
|
||||
void WideNESModule::ppu_write_start_handler(u16 addr, u8 val) {
|
||||
(void)addr;
|
||||
(void)val;
|
||||
// nothing... for now
|
||||
}
|
||||
|
||||
void WideNESModule::ppu_write_end_handler(u16 addr, u8 val) {
|
||||
const PPU& ppu = this->gui.nes._ppu();
|
||||
const PPU::Registers& ppu_reg = ppu._reg();
|
||||
|
||||
|
@ -194,17 +205,29 @@ void WideNESModule::ppu_write_start_handler(u16 addr, u8 val) {
|
|||
|
||||
switch (addr) {
|
||||
case PPUSCROLL: {
|
||||
if (ppu_reg.scroll_latch == 0) this->curr_scroll.x = val;
|
||||
if (ppu_reg.scroll_latch == 1) this->curr_scroll.y = val;
|
||||
}
|
||||
if (ppu_reg.scroll_latch == 1) this->h.ppuscroll.curr.x = val;
|
||||
if (ppu_reg.scroll_latch == 0) this->h.ppuscroll.curr.y = val;
|
||||
} break;
|
||||
case PPUADDR: {
|
||||
this->h.ppuaddr.did_change = true;
|
||||
|
||||
if (ppu._scanline() < 241 && ppu_reg.ppumask.is_rendering)
|
||||
fprintf(stderr, "%3u | 0x%04X <- 0x%02X\n", ppu._scanline(), addr, val);
|
||||
|
||||
this->h.ppuaddr.changed.while_rendering = ppu_reg.ppumask.is_rendering;
|
||||
this->h.ppuaddr.changed.on_scanline = ppu._scanline();
|
||||
|
||||
if (ppu_reg.scroll_latch == 1) this->h.ppuaddr.new_scroll.x = 8 * ppu_reg.t.coarse_x;
|
||||
if (ppu_reg.scroll_latch == 0) this->h.ppuaddr.new_scroll.y = 8 * ppu_reg.t.coarse_y;
|
||||
} break;
|
||||
}
|
||||
}
|
||||
|
||||
void WideNESModule::mmc3_irq_handler(Mapper_004* mmc3, bool active) {
|
||||
this->heuristics.mmc3_irq.curr_scroll_pre_irq = this->curr_scroll;
|
||||
void WideNESModule::mmc3_irq_handler(Mapper_004* mmc3, bool irq_enabled) {
|
||||
this->h.mmc3_irq.scroll_pre_irq = this->h.ppuscroll.curr;
|
||||
|
||||
this->heuristics.mmc3_irq.happened = true;
|
||||
this->heuristics.mmc3_irq.on_scanline = active
|
||||
this->h.mmc3_irq.happened = true;
|
||||
this->h.mmc3_irq.on_scanline = irq_enabled
|
||||
? mmc3->peek_irq_latch()
|
||||
: 239; // cancels out later, give 0 padding
|
||||
}
|
||||
|
@ -219,38 +242,64 @@ void WideNESModule::ppu_frame_end_handler() {
|
|||
|
||||
/*----------------------------- Heuristics -----------------------------*/
|
||||
|
||||
// 1) if the left-column bit is enabled, odds are the game is hiding visual
|
||||
// artifacts, so we can slice that bit off.
|
||||
const u8 ppumask = ppu.peek(0x2001);
|
||||
bool bgr_left_col_mask_disabled = ppumask & 0x02;
|
||||
if (!bgr_left_col_mask_disabled) {
|
||||
this->pad.guess.l = 8;
|
||||
this->pad.guess.r = 8;
|
||||
} else {
|
||||
this->pad.guess.l = 0;
|
||||
this->pad.guess.r = 0;
|
||||
}
|
||||
// - the OG - set current scroll values based off of the PPUSCROLL register
|
||||
this->curr_scroll.x = this->h.ppuscroll.curr.x;
|
||||
this->curr_scroll.y = this->h.ppuscroll.curr.y;
|
||||
|
||||
// 2) Mappers sometimes use a scanline IRQ to split the screen, many times for
|
||||
// making a static status bar at the bottom of the screen (kirby, smb3)
|
||||
// TODO: make this more robust, i.e: get rid of false positives (Megaman IV)
|
||||
if (this->heuristics.mmc3_irq.happened) {
|
||||
this->pad.guess.b = 239 - this->heuristics.mmc3_irq.on_scanline;
|
||||
// depending on if the menu is at the top / bottom of the screen, different
|
||||
// scroll values should be used
|
||||
if (this->heuristics.mmc3_irq.on_scanline < 240 / 2) {
|
||||
// top of screen
|
||||
this->curr_scroll = this->curr_scroll; // stays the same
|
||||
} else {
|
||||
// bottom of screen
|
||||
this->curr_scroll = this->heuristics.mmc3_irq.curr_scroll_pre_irq;
|
||||
// - if the left-column bit is enabled, odds are the game is hiding visual
|
||||
// artifacts, so we can slice that bit off.
|
||||
this->pad.guess.l = ppu._reg().ppumask.m ? 0 : 8;
|
||||
|
||||
// - Zelda scrolling: writes to PPUADDR midframe are probably some creative
|
||||
// scroll implementation?
|
||||
if (this->h.ppuaddr.did_change) {
|
||||
// ppuaddr is written to a lot, but we only care about writes that occur
|
||||
// mid-frame, since those are the one that lead to scrolling / status bars.
|
||||
if (this->h.ppuaddr.changed.on_scanline < 241 &&
|
||||
this->h.ppuaddr.changed.while_rendering) {
|
||||
// note that this heuristic is in-play, and mark the scanline...
|
||||
this->h.ppuaddr.active = true;
|
||||
this->h.ppuaddr.cut_scanline = this->h.ppuaddr.changed.on_scanline;
|
||||
|
||||
// lob off the chunk of the screen not being scrolled...
|
||||
if (this->h.ppuaddr.cut_scanline < 240 / 2) {
|
||||
// top of screen
|
||||
this->pad.guess.t = this->h.ppuaddr.cut_scanline;
|
||||
} else {
|
||||
// bottom of screen
|
||||
this->pad.guess.b = 239 - this->h.mmc3_irq.on_scanline;
|
||||
}
|
||||
|
||||
// set the scroll...
|
||||
this->curr_scroll.y = this->h.ppuaddr.new_scroll.y;
|
||||
}
|
||||
}
|
||||
this->heuristics.mmc3_irq.happened = false; // reset
|
||||
this->h.ppuaddr.did_change = false; // reset each frame
|
||||
|
||||
// 3) vertically scrolling + vertical mirroring usually leads to artifacting
|
||||
// at the top of the screen
|
||||
// ??? not sure about this one...
|
||||
|
||||
// - Mappers sometimes use a scanline IRQ to split the screen, many times for
|
||||
// making a static status bar at the bottom of the screen (kirby, smb3)
|
||||
// TODO: make this more robust, i.e: get rid of false positives (Megaman IV)
|
||||
if (this->h.mmc3_irq.happened) {
|
||||
// depending on if the menu is at the top / bottom of the screen, different
|
||||
// scroll values should be used, and different parts of the screen should be
|
||||
// cut-off
|
||||
if (this->h.mmc3_irq.on_scanline < 240 / 2) {
|
||||
// top of screen
|
||||
this->pad.guess.t = this->h.mmc3_irq.on_scanline;
|
||||
// this->curr_scroll stays the same
|
||||
} else {
|
||||
// bottom of screen
|
||||
this->pad.guess.b = 239 - this->h.mmc3_irq.on_scanline;
|
||||
this->curr_scroll = this->h.mmc3_irq.scroll_pre_irq;
|
||||
}
|
||||
}
|
||||
this->h.mmc3_irq.happened = false; // reset each frame
|
||||
|
||||
// - vertically scrolling + vertical mirroring usually leads to artifacting
|
||||
// at the top of the screen
|
||||
// TODO: implement me
|
||||
// TODO: implement inverse
|
||||
|
||||
/*------------------ Padding / Scrolling Calculations ------------------*/
|
||||
|
||||
|
@ -262,30 +311,38 @@ void WideNESModule::ppu_frame_end_handler() {
|
|||
|
||||
// calculate the new scroll position
|
||||
|
||||
int dx = this->curr_scroll.x - this->last_scroll.x;
|
||||
int dy = this->curr_scroll.y - this->last_scroll.y;
|
||||
int scroll_dx = this->curr_scroll.x - this->last_scroll.x;
|
||||
int scroll_dy = this->curr_scroll.y - this->last_scroll.y;
|
||||
|
||||
const int fuzz = 10;
|
||||
const int thresh_w = (256 - this->pad.total.l - this->pad.total.r) - fuzz;
|
||||
const int thresh_h = (240 - this->pad.total.t - this->pad.total.b) - fuzz;
|
||||
|
||||
// 255 -> 0 | dx is negative | means we are going right
|
||||
// 0 -> 255 | dx is positive | means we are going left
|
||||
if (::abs(dx) > thresh_w) {
|
||||
if (dx < 0) dx += 256; // going right
|
||||
else dx -= 256; // going left
|
||||
// 255 -> 0 | scroll_dx is negative | means we are going right
|
||||
// 0 -> 255 | scroll_dx is positive | means we are going left
|
||||
if (::abs(scroll_dx) > thresh_w) {
|
||||
if (scroll_dx < 0) scroll_dx += 256; // going right
|
||||
else scroll_dx -= 256; // going left
|
||||
}
|
||||
// 239 -> 0 | dy is negative | means we are going down
|
||||
// 0 -> 239 | dy is positive | means we are going up
|
||||
if (::abs(dy) > thresh_h) {
|
||||
if (dy < 0) dy += 240; // going down
|
||||
else dy -= 240; // going up
|
||||
// 239 -> 0 | scroll_dy is negative | means we are going down
|
||||
// 0 -> 239 | scroll_dy is positive | means we are going up
|
||||
if (::abs(scroll_dy) > thresh_h) {
|
||||
if (scroll_dy < 0) scroll_dy += 240; // going down
|
||||
else scroll_dy -= 240; // going up
|
||||
}
|
||||
|
||||
this->scroll.x += dx;
|
||||
this->scroll.y += dy;
|
||||
this->scroll.dx = dx;
|
||||
this->scroll.dy = dy;
|
||||
// Zelda scroll heuristic
|
||||
// not entirely sure why this jump happens... but this fixes it?
|
||||
if (this->h.ppuaddr.active) {
|
||||
if (::abs(scroll_dy) > (int)this->h.ppuaddr.cut_scanline) {
|
||||
scroll_dy = 0;
|
||||
}
|
||||
}
|
||||
|
||||
this->scroll.x += scroll_dx;
|
||||
this->scroll.y += scroll_dy;
|
||||
this->scroll.dx = scroll_dx;
|
||||
this->scroll.dy = scroll_dy;
|
||||
|
||||
this->last_scroll.x = this->curr_scroll.x;
|
||||
this->last_scroll.y = this->curr_scroll.y;
|
||||
|
@ -482,7 +539,7 @@ void WideNESModule::output() {
|
|||
sprintf(buf,
|
||||
"total scroll: %-3d %-3d\n"
|
||||
" last scroll: %-3d %-3d\n"
|
||||
" dx/dy: %-3d %-3d\n",
|
||||
" dx dy: %-3d %-3d\n",
|
||||
this->scroll.x, this->scroll.y,
|
||||
this->last_scroll.x, this->last_scroll.y,
|
||||
this->scroll.dx, this->scroll.dy
|
||||
|
|
|
@ -14,12 +14,27 @@
|
|||
|
||||
class WideNESModule : public GUIModule {
|
||||
private:
|
||||
/*--------------------------- SDL / GUI stuff ----------------------------*/
|
||||
|
||||
struct {
|
||||
SDL_Window* window = nullptr;
|
||||
SDL_Renderer* renderer = nullptr;
|
||||
SDL2_inprint* inprint = nullptr;
|
||||
} sdl;
|
||||
|
||||
SDL_Texture* nes_screen; // copy of actual NES screen
|
||||
|
||||
// zoom/pan info
|
||||
struct {
|
||||
bool active = false;
|
||||
struct { int x; int y; } last_mouse_pos { 0, 0 };
|
||||
int dx = 0;
|
||||
int dy = 0;
|
||||
float zoom = 2.0;
|
||||
} pan;
|
||||
|
||||
/*---------------------------- Tile Rendering ----------------------------*/
|
||||
|
||||
struct Tile {
|
||||
int x, y;
|
||||
|
||||
|
@ -42,20 +57,6 @@ private:
|
|||
// tilemap
|
||||
std::map<int, std::map<int, Tile*>> tiles;
|
||||
|
||||
SDL_Texture* nes_screen;
|
||||
|
||||
// used to calculate dx and dy b/w frames
|
||||
struct nes_scroll {
|
||||
u8 x; u8 y;
|
||||
} last_scroll { 0, 0 }
|
||||
, curr_scroll { 0, 0 };
|
||||
|
||||
// total scroll (offset from origin)
|
||||
struct {
|
||||
int x, y;
|
||||
int dx, dy;
|
||||
} scroll { 0, 0, 0, 0 };
|
||||
|
||||
// there tend to be graphical artifacts at the edge of the screen, so it's
|
||||
// prudent to sample sliglty away from the edge.
|
||||
// moreover, some games have static menus on screen that impair sampling
|
||||
|
@ -64,29 +65,59 @@ private:
|
|||
struct {
|
||||
int l, r, t, b;
|
||||
} guess { 0, 0, 0, 0 } // intelligent guess
|
||||
, offset { 0, 0, 0, 0 } // manual offset
|
||||
, offset { 0, 0, 0, 0 } // user offset
|
||||
, total { 0, 0, 0, 0 }; // sum of the two
|
||||
} pad;
|
||||
|
||||
struct nes_scroll { u8 x; u8 y; };
|
||||
nes_scroll last_scroll { 0, 0 };
|
||||
nes_scroll curr_scroll { 0, 0 };
|
||||
|
||||
// total scroll (offset from origin)
|
||||
struct {
|
||||
int x, y;
|
||||
int dx, dy;
|
||||
} scroll { 0, 0, 0, 0 };
|
||||
|
||||
/*------------------------------ Heuristics ------------------------------*/
|
||||
|
||||
struct {
|
||||
// The OG heuristic: Sniffing the PPUSCROLL registers for changes
|
||||
struct {
|
||||
nes_scroll curr { 0, 0 };
|
||||
} ppuscroll;
|
||||
|
||||
// MMC3 Interrupt handling (i.e: intelligently chopping the screen)
|
||||
// (SMB3, M.C Kids)
|
||||
struct {
|
||||
bool happened = false;
|
||||
uint on_scanline = 239;
|
||||
nes_scroll curr_scroll_pre_irq = { 0, 0 };
|
||||
nes_scroll scroll_pre_irq = { 0, 0 };
|
||||
} mmc3_irq;
|
||||
} heuristics;
|
||||
|
||||
// zoom/pan info
|
||||
struct {
|
||||
bool active = false;
|
||||
struct { int x; int y; } last_mouse_pos { 0, 0 };
|
||||
int dx = 0;
|
||||
int dy = 0;
|
||||
float zoom = 2.0;
|
||||
} pan;
|
||||
// PPUADDR mid-frame changes
|
||||
// (Zelda)
|
||||
struct {
|
||||
bool did_change = false;
|
||||
struct {
|
||||
uint on_scanline = 0;
|
||||
bool while_rendering = 0;
|
||||
} changed;
|
||||
|
||||
bool active = false;
|
||||
|
||||
uint frame_counter = 0; // TEMP: should not be needed with scene detection
|
||||
|
||||
uint cut_scanline = 0;
|
||||
nes_scroll new_scroll = { 0, 0 };
|
||||
} ppuaddr;
|
||||
} h;
|
||||
|
||||
/*--------------------------- Callback Handlers --------------------------*/
|
||||
|
||||
void ppu_frame_end_handler();
|
||||
void ppu_write_start_handler(u16 addr, u8 val);
|
||||
void ppu_write_end_handler(u16 addr, u8 val);
|
||||
|
||||
void mmc3_irq_handler(Mapper_004* mapper, bool active);
|
||||
|
||||
|
@ -94,6 +125,7 @@ private:
|
|||
|
||||
static void cb_ppu_frame_end(void* self);
|
||||
static void cb_ppu_write_start(void* self, u16 addr, u8 val);
|
||||
static void cb_ppu_write_end(void* self, u16 addr, u8 val);
|
||||
|
||||
static void cb_mmc3_irq(void* self, Mapper_004* mapper, bool active);
|
||||
|
||||
|
|
32
wideNES.md
32
wideNES.md
|
@ -8,7 +8,7 @@ That's a lot of work.
|
|||
|
||||
Wouldn't it be cool to automate that?
|
||||
|
||||
Enter **WideNES**, a novel method to map-out NES games automatically.
|
||||
Enter **wideNES**, a novel method to map-out NES games automatically.
|
||||
|
||||
<p align="center">
|
||||
<img src="resources/web/wideNES_smb1.gif" alt="wideNES on SMB1">
|
||||
|
@ -22,13 +22,11 @@ Pretty cool huh? Here's another one:
|
|||
|
||||
## Enabling wideNES
|
||||
|
||||
At the moment, wideNES is activated by default.
|
||||
|
||||
_this is not final_
|
||||
You can enable wideNES by passing the `--widenes` flag from the command-line.
|
||||
|
||||
## Controls
|
||||
|
||||
wideNES inherits all controls from ANESE, and adds a few more:
|
||||
wideNES inherits all controls from ANESE, and adds a few additional ones:
|
||||
|
||||
#### Pan and Zoom
|
||||
|
||||
|
@ -36,10 +34,9 @@ You can **pan** and **zoom** using the mouse + mousewheel.
|
|||
|
||||
#### Padding controls
|
||||
|
||||
Many games have static sections of the screen / artifacts at the edge of the
|
||||
screen. wideNES uses some heuristics (if the PPUMASK bgr-mask is active, mapper
|
||||
IRQs, etc...) to intelligently "guess" padding values, but there are times when
|
||||
a little bit of tweaking is needed...
|
||||
wideNES has many built-in heuristics that are used to "guess" what parts of the
|
||||
screen are not part of the level (i.e: status bars / leave artifacts), and while
|
||||
these work pretty well, there are times some manual tweaking might be preferred.
|
||||
|
||||
Side | increase | decrease
|
||||
--------|----------|-------
|
||||
|
@ -63,7 +60,7 @@ entire screen scroll by a certain amount.
|
|||
WideNES watches the scroll register for changes, and when values are written to
|
||||
it, the deltas between values at different frames (plus some heuristics) are
|
||||
used to intelligently "sample" the framebuffer. Gradually, as the player moves
|
||||
around the world, more and more of the screne is revealed (and saved).
|
||||
around the world, more and more of the screen is revealed (and saved).
|
||||
|
||||
That's it really!
|
||||
|
||||
|
@ -76,8 +73,10 @@ _The Legend of Zelda_. Although it does in fact use the scroll register to do
|
|||
left-and-right screen transitions, it uses a custom technique to do up-and-down
|
||||
screen transitions, never touching the scroll register.
|
||||
|
||||
With it's limited knowledge of the NES, wideNES cannot account for such cases
|
||||
at the moment. (although it is something I would like to look into)
|
||||
I have implemented a heuristic for getting _The Legend of Zelda_ working, but it
|
||||
involved a non-trivial amount of work. Moreover, I have not tested enough games
|
||||
to confidently say the heuristic is game-agnostic (although it is written to
|
||||
apply to a wide-variety of games... in theory)
|
||||
|
||||
#### Sprites as Background Elements
|
||||
|
||||
|
@ -91,12 +90,12 @@ wideNES assumes that if you go in a circle, you end up where you started.
|
|||
Most games follow this rule, but there are exceptions. For example, the Lost
|
||||
Woods in _The Legend of Zelda_.
|
||||
|
||||
I am not sure if I can work around this, but I might try...
|
||||
I haven't looked into this yet, so I cannot be sure if there is a work-around.
|
||||
|
||||
## Roadmap
|
||||
|
||||
- [x] avoid smearing in some games
|
||||
- [x] mask-off edges (artifacting / satic menus)
|
||||
- [x] mask-off edges (artifacting / static menus)
|
||||
- [x] manually
|
||||
- [x] automatically - _using heuristics, not completely perfect_
|
||||
- [ ] Save/Load tiles
|
||||
|
@ -106,6 +105,5 @@ I am not sure if I can work around this, but I might try...
|
|||
- right now, they overwrite tile data
|
||||
- [ ] Animated background / tile support
|
||||
- only sample in 8 pixel chunks?
|
||||
- [ ] Handle non-hardware scrolling (?)
|
||||
|
||||
## Successes
|
||||
- [x] Handle non-hardware scrolling
|
||||
- Partially, works with _TLOZ_
|
||||
|
|
Loading…
Reference in a new issue