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:
Daniel Prilik 2018-07-17 15:30:45 -07:00
parent fc350a64ad
commit cfdb31e6dc
3 changed files with 183 additions and 96 deletions

View file

@ -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

View file

@ -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);

View file

@ -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_