don't click here

Gens Rerecording / Lua issue

Discussion in 'Engineering & Reverse Engineering' started by Mercury, Sep 4, 2014.

  1. Mercury

    Mercury

    His Name Is Sonic Tech Member
    1,740
    21
    18
    Location Location
    AeStHete
    I'm encountering some strange behaviour in Gens Rerecording with Lua.

    I wanted to start making something simple that drew Sonic's collision mask on top of his sprite, so I got the camera coordinates and Sonic's coordinates, subtracted the former from the latter, and drew a box at the resulting position. No matter what I did, though, the box would always flicker and jump around randomly.

    It turned out this was because memory.readword() and its ilk were giving me incorrect values, and I have made a test script to demonstrate (meant for Sonic 2).

    Code (Text):
    1.  
    2. timer = 0;
    3. timerp = 0;
    4. diff = false;
    5. count = 0;
    6.  
    7. gens.registerafter( function()
    8.  
    9.     timerp = timer;
    10.     timer = memory.readword(0xfffe04);
    11.     if (timer == timerp) then diff = true;
    12.     else diff = false;
    13.     end
    14. end)
    15.  
    16. gui.register( function ()
    17.  
    18.     message = string.format("Timer: $%X, %d", timer, count)
    19.     gui.text(10, 50, message, "#FFFF00FF", "black")
    20.    
    21.     if (diff) then gui.drawbox(0, 0, 320, 224, "black", "black");
    22.     count = count + 1;
    23.     end
    24. end)
    25.  
    Because the game timer is supposed to count up by 1 every frame, it's easy to tell if memory.readword() is reading the new value correctly or not. Each time it gets it wrong (and returns the value from the last frame), I increase a counter and make the screen flash black to make it obvious this is happening.

    It doesn't matter if I try to get the value in gens.registerafter(), gens.registerbefore(), or in gui.register(), or even if I put hooks in when the memory is written to. I will still get a bad value from 1 frame ago sometimes.

    Oddly, this seems to only happen when the screen is scrolling. If you stand Sonic still, it won't happen.

    Furthermore, it's not just my Lua code. The RAM watch feature has the same issue, which can be seen if you watch an increasing value such as the timer and use frame advance, sometime it will not update.

    I'm pretty much at my wits' end when it comes to this, because it prevents any kind of useful overlays on objects, making it a showstopping issue. Has anyone here ever experienced this, and do you know what might be the cause or the solution?

    (It happens in Gens Rerecording v11a as well as the latest version at Google code.)
     
  2. flamewing

    flamewing

    Emerald Hunter Tech Member
    1,161
    65
    28
    France
    Sonic Classic Heroes; Sonic 2 Special Stage Editor; Sonic 3&K Heroes (on hold)
    I don't know about preventing useful overlays; but maybe you can treat it as a bug and report it over at their Google code project?
     
  3. Mercury

    Mercury

    His Name Is Sonic Tech Member
    1,740
    21
    18
    Location Location
    AeStHete
    Oh, I only meant overlays on objects. When it comes to stationary HUDs there are lots of good scripts.

    I don't know how active the project still is, but I will report it and see what comes of it.
     
  4. Sappharad

    Sappharad

    Oldbie
    1,413
    70
    28
    If Gens continues to frustrate you, why don't you try writing the same script for BizHawk instead? It supports Lua, and it uses Genesis Plus GX which should be far more accurate than Gens. Since it's under active development you'd probably be more likely to get anything fixed that you need. One of the example genesis scripts that comes with it draws hitboxes for castlevania, so you'd at least have a sample to work from.

    Sorry if this isn't something you wanted to hear, I just like to take the opportunity to promote it whenever I can. (I maintain the Mac OS X port of it, which doesn't have Lua, but I'll probably add it some day)
     
  5. flamewing

    flamewing

    Emerald Hunter Tech Member
    1,161
    65
    28
    France
    Sonic Classic Heroes; Sonic 2 Special Stage Editor; Sonic 3&K Heroes (on hold)
    Ah, so it is you that maintain that port. I have been trying to get it to work on Linux, with zero success; which is why I haven't mentioned it at all.
     
  6. Sappharad

    Sappharad

    Oldbie
    1,413
    70
    28
    Going off topic now, but both Genesis and Lua won't work on Linux anyway even if you get it to run. All of the native cores like Genesis Plus are separate DLL's on the windows version, and they need to be ported to work on Linux. Genesis Plus and a few of the other native cores work on OS X because I've done native builds of them. (.dylib on OS X, .so on linux) In the case of Genesis Plus, getting it to work on Linux should be pretty easy. The code basically compiles out of the box, you just need to set up a build for it somehow. On OS X I made Xcode projects for everything, so it was just a matter of opening the project and hitting build.

    The current Lua implementation uses managed c++, which isn't supported in Mono. At some point I'd like to switch it to use Pinvoke so it can be called the same way Genesis Plus and the other native cores are. If I ever do that, it would work the same as any of the cores where as long as it finds an .so or .dylib where windows expects a DLL, it will just work.

    Having said all of that, the TasVideos Genesis forum might be a better place to get help with re-recording emulators if nobody here knows the answer.
     
  7. Mr Lange

    Mr Lange

    A wise guy eh. I know how to DEAL with wise guys. Member
    1,286
    11
    18
    The Land of Waldos
    Sonic Utopia, Sonic Overture
    Thank you for bringing this up Mercury because this explains exactly the issue I've had. Me and a friend made a lua script a couple years ago that was attempting to output Sonic's position and the Camera's position every frame into a script in order to match it to footage in another program. The position data would always come out skewed and not match the footage. We kept thinking the formula interpreting the data was wrong and any tweaks would just result in a different kind of skewing even when sure it would work. Now I know it wasn't the script's fault.
     
  8. nineko

    nineko

    I am the Holy Cat Tech Member
    6,298
    475
    63
    italy
    lol, I can't even read "tasvideos" and "help" in the same sentence with a straight face, as elitists as they are (minus a few rare exception) I wonder why the forums are still open for new registrations at all when they'd clearly just like to ban everyone.
     
  9. Tribeam

    Tribeam

    I code Lua and Lua accessories Member
    80
    0
    6
    I have no idea whats going on here. I have done alot of testing and coding to try to figure this out to no results, my guess is that this is a gens rerecording bug.

    Edit:
    In my debugEx script, this also happens, but ONLY for the player object, all other objects seem synced up just fine, as demonstrated by this gif:
    (the second box on the player is the shield obj)
    [​IMG]
     
  10. ZanaGB

    ZanaGB

    Member
    9
    0
    1
    Well. I am not the most knowledgeable person regarding how this goes. but I definetly know that BizHawk offers support for LUA scripting and a far more accurate emulation core. Maybe you should try rewritting the script for BH, since it looks like Gens is not doing it's job properly ( as usual ). I do not think rewritting it should be much of an issue though.
     
  11. Mercury

    Mercury

    His Name Is Sonic Tech Member
    1,740
    21
    18
    Location Location
    AeStHete
    Thanks for recommending BizHawk, I'd never heard of it and it's great to have more options. Unfortunately, though, when I rewrote the script for BizHawk and tried it out, the exact same thing happened - only worse, as it now happens even when the screen is not scrolling.

    Glad to have helped clear up such a long-standing mystery!

    I'm starting to think it might have something to do with the Sonic engine itself, and maybe the values being looked at aren't correct at the end of each frame? (Also, no problem about not answering my PM right away. :) )

    In good(ish) news I've made a little bit of progress. I realised I could use GUI functions in any event, so I put the code in an event that occurs when the timer is updated. Now it works, moving synchronously with Sonic (more or less; it's still one frame off, but that's merely because of when the timer is updated).

    However, even though I'm now getting the correct values, the GUI fails to draw sometimes, causing the overlay to flicker. It's better than jumping around, so that's something!

    Here's the new script (for Gens, still):

    Code (Text):
    1. timer = 0;
    2. timer_prev = 0;
    3.  
    4. problem_count = 0;
    5. write_count = 0;
    6.  
    7. memory.registerwrite(0xFFFE04, 2, function()
    8.     write_count = write_count + 1;
    9.    
    10.     cam_x = memory.readword(0xffee00);
    11.     cam_y = memory.readword(0xffee04);
    12.    
    13.     sonic_x = memory.readword(0xffb008);
    14.     sonic_y = memory.readword(0xffb00c);
    15.    
    16.     sonic_radius_x = memory.readbytesigned(0xffb017);
    17.     sonic_radius_y = memory.readbytesigned(0xffb016);
    18.    
    19.     sonic_screen_x = sonic_x - cam_x;
    20.     sonic_screen_y = sonic_y - cam_y;
    21.    
    22.     timer_prev = timer;
    23.     timer = memory.readword(0xFFFE04);
    24.    
    25.     if (timer == timer_prev) then
    26.         gui.drawbox(0, 0, 320, 224, 0x00000080, 0x00000080);
    27.         problem_count = problem_count + 1;
    28.     end;
    29.    
    30.     gui.drawbox(sonic_screen_x - sonic_radius_x,
    31.         sonic_screen_y - sonic_radius_y,
    32.         sonic_screen_x + sonic_radius_x,
    33.         sonic_screen_y + sonic_radius_y,
    34.         0xFFFFFF3F, 0xFFFFFFFF);
    35.    
    36.     gui.drawbox(sonic_screen_x - 10,
    37.         sonic_screen_y,
    38.         sonic_screen_x + 10,
    39.         sonic_screen_y,
    40.         0xFFFFFFFF);
    41.    
    42.     message = string.format("Timer: $%X\nWrites: %d\nProblems: %d", timer, write_count, problem_count);
    43.     gui.text(16, 64, message, 0xFFFF00FF, 0x000000FF);
    44. end);
     
  12. Tribeam

    Tribeam

    I code Lua and Lua accessories Member
    80
    0
    6
    So I took a brief look at BizHawk, and I have to say it has definitely taken my attention away from gens rerecording, the lua library it offers looks far more feature rich...I shall post here with results as I mess with it...

    Edit
    I'm starting to think it's with the sonic engine as well, in my debug script, I've noticed that sonic's position seem to do the same thing as the level counter(same a previous frame, randomly) but other positional data doesn't do this, such as the camera's position, and all other objs, interesting....
     
  13. Sappharad

    Sappharad

    Oldbie
    1,413
    70
    28
    If you can provide a simplified example either here or privately, I can submit a bug report for it if you don't want to. And perhaps look at the code, but no promises on that. I've seen them add random lua functionality that people have asked for rather quickly, so I think the odds are reasonable that if there's no technical reason why it behaves that way they'd probably want to see it fixed.
     
  14. Tribeam

    Tribeam

    I code Lua and Lua accessories Member
    80
    0
    6
    This doesn't happen nearly as often in Sonic3K as it does in Sonic2, and in Sonic1, it's pretty much every other frame, further pushing the idea it's an engine issue.
    This happens in both emulators.

    Here's a Bizhawk Lua script that puts a box over every obj in Sonic3K
    This shows that the GUI is kinda lagged behind on the current frame, also the player's box has a jitter, but is FAR less jittery than Gens.

    Code (Text):
    1.  
    2. --Object Identifier thing by Tribeam
    3.  
    4. local oram_start = 0xB000
    5. local oram_end = 0xCFCB
    6. local cxram = 0xEE78
    7. local cyram = 0xEE7C
    8. local oram_size = oram_end-oram_start
    9. local camx = 0
    10. local camy = 0
    11. local xd = 0
    12. local yd = 0
    13. local x1 = 0
    14. local y1 = 0
    15. local x2 = 0
    16. local y2 = 0
    17. local objcount = 0
    18.  
    19. while true do
    20.     emu.frameadvance()
    21.     objcount = 0
    22.     camx = memory.read_u16_le(cxram)
    23.     camy = memory.read_u16_le(cyram)
    24.     for I = oram_start, oram_end, 0x4A do
    25.         if memory.read_u32_le(I) ~= 0 then
    26.             xd = memory.read_u16_le(I+0x10) - camx
    27.             yd = memory.read_u16_le(I+0x14) - camy
    28.             x1 = xd - 6
    29.             y1 = yd - 6
    30.             x2 = xd + 6
    31.             y2 = yd + 6
    32.             gui.drawBox(x1, y1, x2, y2)
    33.             objcount = objcount + 1
    34.         end
    35.     end
    36.     gui.text(30, 110, "obj count: " .. objcount)
    37. end
    38.  

    This is a BizHawk version of mercury's level frame counter tester, this works in sonic1, 2, and 3(k)
    Code (Text):
    1.  
    2. timer = 0;
    3. timerp = 0;
    4. diff = false;
    5. count = 0;
    6.  
    7. while true do
    8.     emu.frameadvance()
    9.    
    10.     timerp = timer
    11.     timer = memory.read_u16_le(0xfe04)
    12.    
    13.     message = string.format("Timer: $%X, %d", timer, count)
    14.     gui.text(10, 100, message, "#FFFF00FF")
    15.    
    16.     if (timer == timerp) then
    17.         gui.drawBox(0, 0, 320, 224, "black", "black");
    18.         count = count + 1;
    19.     end
    20. end
    21.  
     
  15. Sappharad

    Sappharad

    Oldbie
    1,413
    70
    28
    For what it's worth, we discussed it a bit on #bizhawk
    [7:07pm] natt: two immediate thoughts pop up
    [7:07pm] sappharad: Or not. I was thinking perhaps there would be cases where it would run a frame and not execute the script, but the code seems to suggest that it always happens
    [7:07pm] natt: 1: he's polling at the wrong time in the frame to use the memory address he's using
    [7:08pm] natt: 2: the memory address doesn't update every frame because it's not the right real one; instead its some sort of special shadow copy or something
    [7:08pm] sappharad: I can post that if you want. I'm assuming the answer applies to both emulators.
    [7:09pm] th2o joined the chat room.
    [7:09pm] Reaper_man joined the chat room.
    [7:10pm] natt: maybe
    [7:10pm] natt: I don't offhand know what either one uses for frame timing
    [7:12pm] adelikat_: we have onframebefore, and onframeafter
    [7:12pm] adelikat_: which is equivlant to register and registerafter
    [7:12pm] sappharad: I was just worried perhaps that if frameskip was on or something, that it might only execute for frames that render. Or might not execute for lag frames. But it looks like that doesn't matter
    [7:12pm] adelikat_: if it works in neither I'm going to assume he has a logical flaw
    [7:12pm] adelikat_: and gens would not behave that way
    [7:12pm] adelikat_: and bizhawk certainly doesn't
    [7:15pm] natt: adelikat_: I meant where in the emulated system's vcount gpgx puts the frame end boundary
    [7:15pm] natt: if he's polling the variable partway through the game engine's internal update process
    [7:16pm] natt: then he very well could miss updates if certain parts of that update process take non-constant time
    [7:16pm] natt: so suppose he polls at line 250 (a vblank line)
    [7:17pm] Fog_TAS left the chat room. (Read error: Connection reset by peer)
    [7:17pm] natt: and at one frame, the variable is updated at line 249
    [7:17pm] natt: in the next frame, its updated at line 251
    [7:17pm] natt: because other actions the cpu performed that frame took a bit of extra time
    [7:17pm] natt: admittedly, this explanation seems unlikely if the frame end is at vblank
    [7:17pm] natt: at the start of vblank*
    [7:18pm] natt: but if he's polling some shadow copy of the variable that's updated as a part of some other routine, it becomes more likely
    [7:18pm] sappharad: How do you know it's a shadow copy? The FF address?
    [7:19pm] sappharad: Maybe he's assuming it's expecting Pro-Action Replay style addresses since PAR RAM codes are prefixed with FF
    [7:19pm] natt: sappharad: just a guess
    [7:19pm] natt: I don't know the memory map of that system offhand
    [7:20pm] sappharad: I'm going to try his script in BizHawk. Hope it's not too difficult to figure out, I've never used Lua
    [7:23pm] natt: might try using the onwrite hooks to see when that variable is actually changed
    [7:23pm] sappharad: Heh… Big long exception dialog is BizHawk's way of telling me that Lua editing is done in an external editor that I don't have. Oops

    I looked at the code for Lua in BizHawk and it does have hooks that execute before and after every frame. I'll try the script above and see if I learn anything.

    Edit: Okay, so at least in the case of BizHawk, this doesn't look like a bug. Just open the memory viewer and watch the values yourself. On the frames that flash black, the timer address does not change. On the following frame, it only goes up by 1. So it's not as if it is skipping a value, it just isn't being updated on that frame. I noticed it never happens when you're standing still, but if you let the spring at the end of EHZ1 just push you up the slope at the end so you endlessly go forward and back down, you'll see it skip updates for a few frames every trip back down.

    I think this is just a case of the game not working the way you think it should. If you can provide any evidence to the contrary, I'm still willing to try and get you help with this.
     
  16. flamewing

    flamewing

    Emerald Hunter Tech Member
    1,161
    65
    28
    France
    Sonic Classic Heroes; Sonic 2 Special Stage Editor; Sonic 3&K Heroes (on hold)
    Oh, I got it -- it is an engine issue, coupled with the definition of a "frame" for emulators. A frame in Gens and BizHawk last, in general, from V-Int to V-Int (or when V-Int would have happened, in the cases where it is disabled).

    Here is what I think is (at least part of) the problem: most of a frame happens during active display; the game updates its logic, decides where sprites are, where the camera is, and calls BuildSprites (or equivalent). Frame finishes rendering for display*, V-Int happens, a frame ends. During V-Int, the data buffered by BuildSprites is transferred to the VDP, as is horizontal scroll based on camera position. This stuff will be rendered during active display, while the game logic will update positions and so forth; at the end of this frame, the image generated the first frame I talked about will be shown. Reading data at this point will give the "wrong" values for sprite positions and camera location relative to the graphics because the graphics are from 2 frames ago -- you are using the new positions and camera location to render overlays on graphics two frames old. Sometimes, they will line up -- but that is by chance, or when you are moving at constant speed.

    Now, the S3&K engine is a lot more optimized than S1 and S2, particularly when it comes to level drawing -- S3&K renders the level during active display to a RAM buffer and transfers the whole thing during vblank. S1 and S2 set flags during active display that say what needs to render, and it renders it all during vblank. And what is more, S1's 68k sound driver also probably gets in the way of it all, making it even worse.

    I have no theory at the moment about the differences between Gens and BizHawk; could be due to accuracy differences or due to how they render the overlays.
     
  17. Sappharad

    Sappharad

    Oldbie
    1,413
    70
    28
    If it helps at all, BizHawk has OnMemoryRead(), OnMemoryWrite() and OnMemoryExecute() Lua callbacks, if you'd rather not be restricted to frame boundaries. You could always hook either the addresses themselves, or the execute on the code that changes them.

    Not sure if Gens has something like that. Here is the documentation for them:
    Code (Text):
    1.  
    2.         [LuaMethodAttributes(
    3.             "onmemoryexecute",
    4.             "Fires after the given address is executed by the core"
    5.         )]
    6.         [LuaMethodAttributes(
    7.             "onmemoryread",
    8.             "Fires after the given address is read by the core. If no address is given, it will attach to every memory read"
    9.         )]
    10.         [LuaMethodAttributes(
    11.             "onmemorywrite",
    12.             "Fires after the given address is written by the core. If no address is given, it will attach to every memory write"
    13.         )]
    14.  
    They all take in an address as argument.
     
  18. ZanaGB

    ZanaGB

    Member
    9
    0
    1
    I am glad to know that my small suggestion has helped some ( at least to further proof that the S2 engine is weird, and to show there are some other capable emulators for recording and scripting over ). Hopefully this can help for the future

    + - I should probably not waste my trial posts like this but it's always good to know when your information can be useful  
     
  19. Mercury

    Mercury

    His Name Is Sonic Tech Member
    1,740
    21
    18
    Location Location
    AeStHete
    It totally is the game, not the emulator. I just tried onmemoryexecute on the V_Int routine, and it now works exactly as I expect.
    Code (Text):
    1. timer = 0;
    2. timer_prev = 0;
    3.  
    4. problem_count = 0;
    5. write_count = 0;
    6.  
    7. memory.registerexec(0x000408, function()
    8.     write_count = write_count + 1;
    9.    
    10.     cam_x = memory.readword(0xffee00);
    11.     cam_y = memory.readword(0xffee04);
    12.    
    13.     sonic_x = memory.readword(0xffb008);
    14.     sonic_y = memory.readword(0xffb00c);
    15.    
    16.     sonic_radius_x = memory.readbytesigned(0xffb017);
    17.     sonic_radius_y = memory.readbytesigned(0xffb016);
    18.    
    19.     sonic_screen_x = sonic_x - cam_x;
    20.     sonic_screen_y = sonic_y - cam_y;
    21.    
    22.     timer_prev = timer;
    23.     timer = memory.readword(0xFFFE04);
    24.    
    25.     if (timer == timer_prev) then
    26.         gui.drawbox(0, 0, 320, 224, 0x00000080, 0x00000080);
    27.         problem_count = problem_count + 1;
    28.     end;
    29.    
    30.     gui.drawbox(sonic_screen_x - sonic_radius_x,
    31.         sonic_screen_y - sonic_radius_y,
    32.         sonic_screen_x + sonic_radius_x,
    33.         sonic_screen_y + sonic_radius_y,
    34.         0xFFFFFF3F, 0xFFFFFFFF);
    35.    
    36.     gui.drawbox(sonic_screen_x - 10,
    37.         sonic_screen_y,
    38.         sonic_screen_x + 10,
    39.         sonic_screen_y,
    40.         0xFFFFFFFF);
    41.    
    42.     message = string.format("Timer: $%X\nWrites: %d\nProblems: %d", timer, write_count, problem_count);
    43.     gui.text(16, 64, message, 0xFFFF00FF, 0x000000FF);
    44. end);
    45.  
    46.  
    47.  
    Thanks for all the help, everybody! This was really confusing the heck out of me.
     
  20. flamewing

    flamewing

    Emerald Hunter Tech Member
    1,161
    65
    28
    France
    Sonic Classic Heroes; Sonic 2 Special Stage Editor; Sonic 3&K Heroes (on hold)
    It has them, yes:

    memory.registerexec

    memory.registerread

    memory.registerwrite

    They all take address, size (number of bytes to watch) and callback as parameters.