Post subject: Discovering a DOS game's frame rate
GabCM
He/Him
Joined: 5/5/2009
Posts: 901
Location: QC, Canada
So far, I've encoded DOS games in 60 fps, but I've heard of higher and variable frame rates. I'm planning to program a little something that would analyse a timecode v2 file, generated by the JPC-RR stream tools, to discover the shortest time between two frames, and to base on that result to find highest frame rate, and use this to create a CFR dump, so I can import it in an AviSynth script without any frame loss. But before I do such a thing, if you have any easier way to convert a VFR DOS dump to a CFR dump with the VFR's highest frame rate, then shoot.
Post subject: Re: Discovering a DOS game's frame rate
Emulator Coder, Skilled player (1141)
Joined: 5/1/2010
Posts: 1217
Mister Epic wrote:
So far, I've encoded DOS games in 60 fps, but I've heard of higher and variable frame rates.
The usual rate for runs using JPC-RR r11.x (every DOS submission from 2871S onwards) is 125875/1796 fps (but not all use that, for instance Jazz Jackrabbit is 125875/2108 fps during action sequences). JPC-RR r10.x runs (2870S was the last of those) always use 60fps. If you use timecode data, be careful as some games may jump between multiple rates and the BIOS bootup is at 125875/1796 fps anyway (and resolution switches can cause timecodes to jump oddly). Also, timecode output resolution is only 1ms. Timecode data at 1ns resolution could be extracted from the dump. Oh, and newer versions (sadly, not the r11.2 that has a pre-built binary package) have a menu entry to show what frame rate the game is currently running with. Oh, here's a script to parse dumps: Download dumpparse.lua
Language: lua

current_segment_table = nil; current_ts = 0; file_offset = 0; describe_type = function(major, minor) return ( (major == 0) and ( "VIDEO_" .. ( (minor == 0) and "UNCOMPRESSED" or (minor == 1) and "COMPRESSED" or ("UNKNOWN" .. minor) ) ) or (major == 1) and ( "PCM_" .. ( (minor == 0) and "VOLUME" or (minor == 1) and "SAMPLE" or ("UNKNOWN" .. minor) ) ) or (major == 2) and ( "FM_" .. ( (minor == 0) and "VOLUME" or (minor == 1) and "LOWWRITE" or (minor == 2) and "HIGHWRITE" or (minor == 3) and "RESET" or ("UNKNOWN" .. minor) ) ) or (major == 3) and ( "DUMMY_SUBTYPE" .. minor ) or (major == 4) and ( "SUBTITLE_" .. ( (minor == 0) and "SUBTITLE" or ("UNKNOWN" .. minor) ) ) or (major == 5) and ( "GAMEINFO_" .. ( (minor == 65) and "AUTHORS" or (minor == 71) and "GAMENAME" or (minor == 76) and "LENGTH" or (minor == 82) and "RERECORDS" or ("UNKNOWN" .. minor) ) ) or (major == 6) and ( "MIDI_" .. ( (minor == 0) and "DATA" or ("UNKNOWN" .. minor) ) ) or ("UNKNOWN" .. major .. "_UNKNOWN" .. minor) ) end conditional_error = function(cond, msg) if cond then error(msg); end end string_to_number = function(str) local i, v; if not str then return nil; end v = 0; for i = 1,#str do v = 256 * v + string.byte(str, i); end return v; end read_fully = function(file, len) local ret = file:read(len) conditional_error(not ret or #ret < len, "Corrupt dump file: Unexpected end of file"); file_offset = file_offset + len; return ret; end read_fully_or_none = function(file, len) local ret = file:read(len) if not ret or ret == "" then return nil; end conditional_error(#ret < len, "Corrupt dump file: Unexpected end of file"); file_offset = file_offset + len; return ret; end read_skip = function(file, len) conditional_error(not file:seek("cur", len), "Corrupt dump file: Can't seek to end of packet"); file_offset = file_offset + len; end print_entry = function(chan_num, ts, minor, offset, length) conditional_error(not current_segment_table, "Corrupt dump file: Entry with no segment table in effect"); conditional_error(chan_num == 0xFFFF, "Internal_error: print_entry() called with chan_num == 0xFFFF"); conditional_error(not current_segment_table[chan_num], "Corrupt dump file: Entry with channel #" .. chan_num .. " not in segment table"); print(current_segment_table[chan_num].name, ts, describe_type(current_segment_table[chan_num].major, minor), offset, length); end read_segment_table_body = function(file) current_segment_table = {}; local entries = string_to_number(read_fully(file, 2)); conditional_error(entries == 0 or entries == 65535, "Corrupt dump file: Illegal number of channels " .. "in segment (" .. entries .. ")"); local i; for i = 1,entries do local chan = string_to_number(read_fully(file, 2)); current_segment_table[chan] = {}; current_segment_table[chan].major = string_to_number(read_fully(file, 2)); local namelen = string_to_number(read_fully(file, 2)); current_segment_table[chan].name = read_fully(file, namelen); end end handle_skip = function(file) current_ts = current_ts + 4294967295; end specials = { [string.char(255, 255, 255, 255)] = handle_skip, ["JPCRRMULTIDUMP"] = read_segment_table_body }; handle_special = function(file) local possible_indices = {}; local i = 1; local mlen = 0; for k, v in pairs(specials) do possible_indices[i] = k; i = i + 1; end while true do local d = read_fully(file, 1); local i = 1; while i <= #possible_indices do if string.sub(possible_indices[i], mlen + 1, mlen + 1) ~= d then table.remove(possible_indices, i); else i = i + 1; end end conditional_error(#possible_indices == 0, "Crroupt dump file: Unknown special type"); mlen = mlen + 1; if #possible_indices == 1 and #possible_indices[1] == mlen then break; end end specials[possible_indices[1]](file); end handle_packet = function(file, chan) if chan == 0xFFFF then return handle_special(file); end local tsdelta = string_to_number(read_fully(file, 4)); local minor = string_to_number(read_fully(file, 1)); local len = 0; while true do local d = string_to_number(read_fully(file, 1)); if d >= 128 then len = 128 * len + d - 128; else len = 128 * len + d; break; end end local offset = file_offset; read_skip(file, len); current_ts = current_ts + tsdelta; print_entry(chan, current_ts, minor, offset, len); end process_stream = function(file) file_offset = 0; while true do local chan = string_to_number(read_fully_or_none(file, 2)); if not chan then return; end handle_packet(file, chan); end end if not arg[1] then error("Filename needed"); end local file, err = io.open(arg[1], "rb"); if not file then error("Can't open " .. arg[1] .. ": " .. err); end process_stream(file); file:close();
Post subject: Re: Discovering a DOS game's frame rate
Editor, Active player (296)
Joined: 3/8/2004
Posts: 7469
Location: Arzareth
While finding the highest frame rate can be useful in ensuring that no frame gets skipped, it is not the way to produce the best quality. To make this easier to understand, suppose that you have got video material that has 3fps video and 4fps video. You see that 4fps is the highest, so you choose 4fps for the video. Now, when the input goes to 3fps mode, you will be producing video that has three frames of new material, one duplicate frame, three frames of new material, one duplicate frame, and so on. The end result is that it looks twitchy. The perfect way would be to find the lowest common multiple (LCM). For 3fps and 4ps, it would be 12fps. For 4fps and 6fps, it would also be 12fps. Because 12 can be divided evenly with 3, 4 and 6. Of course, it can get awkward for arbitrary rates; for example, the LCM for 125875/1796 fps and 125875/2108 fps is 31468.75 fps, which is unpractical. Instead of a mathematically correct LCM, it might make sense to try to find an outfps where for all infps, it holds that outfps/infps ≥ 1 and lim(abs(0.5−frac(outfps/infps)) → 0.5) and outfps ≤ max(200, max(infps)).
Banned User, Former player
Joined: 3/10/2004
Posts: 7698
Location: Finland
Is a video container file format forced to have the same framerate throughout, or can you change framerates?
Post subject: Re: Discovering a DOS game's frame rate
Joined: 11/4/2007
Posts: 1772
Location: Australia, Victoria
Bisqwit: This is why we dump Nintendo 64 games at 120fps and DeDup them in AviSynth, to drop frames that are duplicated. And there has been absolutely no twitch whatsoever with the varying FPS material that I have processed, that I have noticed, anyway. Similary, if it was up to me, I'd dump DOS games at frame rates of up to 200fps (Note, I haven't actually encoded a DOS game myself so I don't claim to be an expert on the subject regarding DOS) and operate under the same philosophy. Warp: The MP4 and MKV formats support variable frame rates.
Post subject: Re: Discovering a DOS game's frame rate
Editor, Active player (296)
Joined: 3/8/2004
Posts: 7469
Location: Arzareth
Flygon wrote:
Bisqwit: This is why we dump Nintendo 64 games at 120fps and DeDup them in AviSynth, to drop frames that are duplicated. And there has been absolutely no twitch whatsoever with the varying FPS material that I have processed, that I have noticed, anyway.
As long as the original framerates are very close to 120, 60, 40, 30, 24, 20, 15 or 12 fps, i.e. integer ratios of 120fps, indeed there is no twitch. But if the source is, say, 50.000 fps, then upscaling to 120.000 fps means that out of every 5 frames, 3 frames are duplicated 2 times and 2 frames are duplicated 3 times. Which means, for input frame pattern "01234", the output will have "001112233344", for a total increase of framerate by a non-integer ratio of 2.4 (120/50). Which means that almost half of the frames are shown for a 50% longer time than the other frames. Of course, given that the input is (in the case of this example) already 50 fps to begin with, the difference is only in order of tens of milliseconds, but it needlessly falls into the "your mileage may vary" land.
Joined: 11/4/2007
Posts: 1772
Location: Australia, Victoria
All frames get dropped to one frame in the end anyway, creating a variable framerate output. :p
Editor, Active player (296)
Joined: 3/8/2004
Posts: 7469
Location: Arzareth
Flygon wrote:
All frames get dropped to one frame in the end anyway, creating a variable framerate output. :p
Yes, but once you have pigeonholed it into a certain output fps, the original timings of the frames are lost. To elaborate, think of this 50 -> 120 case:
original 50 fps   upscaled 120 fps    deldupped vfps

frame 0 @ 0ms	  frame 0 @ 0ms	    frame 0 @ 0ms
                  frame 0 @ 8.3ms
frame 1 @ 20ms    frame 1 @ 16.7ms    frame 1 @ 16.7ms (delta=16.7ms)
                  frame 1 @ 25ms
                  frame 1 @ 33.3ms
frame 2 @ 40ms    frame 2 @ 41.67ms   frame 2 @ 41.67ms (delta=25ms)
                  frame 2 @ 50ms
frame 3 @ 60ms    frame 3 @ 58.3ms    frame 3 @ 58.3ms (delta=16.7ms)
                  frame 3 @ 66.67ms
                  frame 3 @ 75ms
frame 4 @ 80ms    frame 4 @ 83.3ms    frame 4 @ 83.3ms (delta=25ms)
                  frame 4 @ 91.67ms
frame 5 @ 100ms   frame 5 @ 100ms     frame 5 @ 100ms (delta=16.7ms)
                  frame 5 @ 108.3ms
frame 6 @ 120ms   frame 6 @ 116.7ms   frame 6 @ 116.7ms (delta=16.7ms)
                  frame 6 @ 125ms
                  frame 6 @ 133.3ms
frame 7 @ 140ms   frame 7 @ 141.67ms  frame 7 @ 141.67ms (delta=25ms)
                  frame 7 @ 150ms
As you can see, the original material had a stable 20 millisecond interval between frames (1/50 seconds). However, due to pigeonholing into material that has a 8.333 millisecond interval between the frames (1/120 seconds), in the deldupped material, which although has an average framerate of 50 fps, the intervals between two consecutive frames end up alternating between 16.67 milliseconds and 25 milliseconds rather than being a stable 20 milliseconds.
Joined: 11/4/2007
Posts: 1772
Location: Australia, Victoria
Ah, I see what you mean now. Either way, I wouldn't be too worried about having staggered framerates. I've seen no users complain, and I've spoken with the publication team about it over time, and indeed, none of them have noticed either.
Joined: 7/29/2011
Posts: 61
Bisqwit wrote:
Of course, it can get awkward for arbitrary rates; for example, the LCM for 125875/1796 fps and 125875/2108 fps is 31468.75 fps, which is unpractical. Instead of a mathematically correct LCM, it might make sense to try to find an outfps where for all infps, it holds that outfps/infps ≥ 1 and lim(abs(0.5−frac(outfps/infps)) → 0.5) and outfps ≤ max(200, max(infps)).
Can you further explain this? I would like to know what the most realistic solution is for this problem.