JMD dumping support
This commit is contained in:
parent
2993d6d793
commit
65f5f37033
4 changed files with 488 additions and 0 deletions
164
avidump/jmd-control.cpp
Normal file
164
avidump/jmd-control.cpp
Normal file
|
@ -0,0 +1,164 @@
|
|||
#include "lua.hpp"
|
||||
#include "jmd.hpp"
|
||||
#include "settings.hpp"
|
||||
#include "misc.hpp"
|
||||
#include <iomanip>
|
||||
#include <cassert>
|
||||
#include <cstring>
|
||||
#include <sstream>
|
||||
#include <zlib.h>
|
||||
#include "misc.hpp"
|
||||
#include "avsnoop.hpp"
|
||||
#include "command.hpp"
|
||||
|
||||
namespace
|
||||
{
|
||||
class jmd_avsnoop : public av_snooper
|
||||
{
|
||||
public:
|
||||
jmd_avsnoop(const std::string& filename, unsigned level) throw(std::bad_alloc)
|
||||
{
|
||||
vid_dumper = new jmd_dumper(filename, level);
|
||||
have_dumped_frame = false;
|
||||
audio_w = 0;
|
||||
audio_n = 0;
|
||||
video_w = 0;
|
||||
video_n = 0;
|
||||
maxtc = 0;
|
||||
}
|
||||
|
||||
~jmd_avsnoop() throw()
|
||||
{
|
||||
delete vid_dumper;
|
||||
}
|
||||
|
||||
void frame(struct lcscreen& _frame, uint32_t fps_n, uint32_t fps_d)
|
||||
throw(std::bad_alloc, std::runtime_error)
|
||||
{
|
||||
struct lua_render_context lrc;
|
||||
render_queue rq;
|
||||
lrc.left_gap = 0;
|
||||
lrc.right_gap = 0;
|
||||
lrc.bottom_gap = 0;
|
||||
lrc.top_gap = 0;
|
||||
lrc.queue = &rq;
|
||||
lrc.width = _frame.width;
|
||||
lrc.height = _frame.height;
|
||||
lua_callback_do_video(&lrc);
|
||||
dscr.reallocate(lrc.left_gap + _frame.width + lrc.right_gap, lrc.top_gap + _frame.height +
|
||||
lrc.bottom_gap, lrc.left_gap, lrc.top_gap, false);
|
||||
dscr.copy_from(_frame, 1, 1);
|
||||
rq.run(dscr);
|
||||
|
||||
vid_dumper->video(get_next_video_ts(fps_n, fps_d), dscr.memory, dscr.width, dscr.height);
|
||||
have_dumped_frame = true;
|
||||
}
|
||||
|
||||
void sample(short l, short r) throw(std::bad_alloc, std::runtime_error)
|
||||
{
|
||||
uint64_t ts = get_next_audio_ts();
|
||||
if(have_dumped_frame)
|
||||
vid_dumper->audio(ts, l, r);
|
||||
}
|
||||
|
||||
void end() throw(std::bad_alloc, std::runtime_error)
|
||||
{
|
||||
vid_dumper->end(maxtc);
|
||||
}
|
||||
|
||||
void gameinfo(const std::string& gamename, const std::list<std::pair<std::string, std::string>>&
|
||||
authors, double gametime, const std::string& rerecords) throw(std::bad_alloc,
|
||||
std::runtime_error)
|
||||
{
|
||||
vid_dumper->gameinfo(gamename, authors, gametime, rerecords);
|
||||
}
|
||||
private:
|
||||
uint64_t get_next_video_ts(uint32_t fps_n, uint32_t fps_d)
|
||||
{
|
||||
uint64_t ret = video_w;
|
||||
video_w += (1000000000ULL * fps_d) / fps_n;
|
||||
video_n += (1000000000ULL * fps_d) % fps_n;
|
||||
if(video_n >= fps_n) {
|
||||
video_n -= fps_n;
|
||||
video_w++;
|
||||
}
|
||||
maxtc = (ret > maxtc) ? ret : maxtc;
|
||||
return ret;
|
||||
}
|
||||
|
||||
uint64_t get_next_audio_ts()
|
||||
{
|
||||
uint64_t ret = audio_w;
|
||||
audio_w += 31210;
|
||||
audio_n += 31990;
|
||||
if(audio_n >= 64081) {
|
||||
audio_n -= 64081;
|
||||
audio_w++;
|
||||
}
|
||||
maxtc = (ret > maxtc) ? ret : maxtc;
|
||||
return ret;
|
||||
}
|
||||
|
||||
jmd_dumper* vid_dumper;
|
||||
screen dscr;
|
||||
unsigned dcounter;
|
||||
bool have_dumped_frame;
|
||||
uint64_t audio_w;
|
||||
uint64_t audio_n;
|
||||
uint64_t video_w;
|
||||
uint64_t video_n;
|
||||
uint64_t maxtc;
|
||||
};
|
||||
|
||||
jmd_avsnoop* vid_dumper;
|
||||
|
||||
function_ptr_command<const std::string&> jmd_dump("dump-jmd", "Start JMD capture",
|
||||
"Syntax: dump-jmd <level> <file>\nStart JMD capture to <file> using compression\n"
|
||||
"level <level> (0-9).\n",
|
||||
[](const std::string& args) throw(std::bad_alloc, std::runtime_error) {
|
||||
tokensplitter t(args);
|
||||
std::string level = t;
|
||||
std::string prefix = t.tail();
|
||||
if(prefix == "")
|
||||
throw std::runtime_error("Expected filename");
|
||||
if(vid_dumper)
|
||||
throw std::runtime_error("JMD dumping already in progress");
|
||||
unsigned long level2;
|
||||
try {
|
||||
level2 = parse_value<unsigned long>(level);
|
||||
if(level2 > 9)
|
||||
throw std::runtime_error("Level must be 0-9");
|
||||
} catch(std::bad_alloc& e) {
|
||||
throw;
|
||||
} catch(std::runtime_error& e) {
|
||||
throw std::runtime_error("Bad JMD compression level '" + level + "': " + e.what());
|
||||
}
|
||||
try {
|
||||
vid_dumper = new jmd_avsnoop(prefix, level2);
|
||||
} catch(std::bad_alloc& e) {
|
||||
throw;
|
||||
} catch(std::exception& e) {
|
||||
std::ostringstream x;
|
||||
x << "Error starting dump: " << e.what();
|
||||
throw std::runtime_error(x.str());
|
||||
}
|
||||
messages << "Dumping to " << prefix << " at level " << level2 << std::endl;
|
||||
});
|
||||
|
||||
function_ptr_command<> end_avi("end-jmd", "End JMD capture",
|
||||
"Syntax: end-jmd\nEnd a JMD capture.\n",
|
||||
[]() throw(std::bad_alloc, std::runtime_error) {
|
||||
if(!vid_dumper)
|
||||
throw std::runtime_error("No video dump in progress");
|
||||
try {
|
||||
vid_dumper->end();
|
||||
messages << "Dump finished" << std::endl;
|
||||
} catch(std::bad_alloc& e) {
|
||||
throw;
|
||||
} catch(std::exception& e) {
|
||||
messages << "Error ending dump: " << e.what() << std::endl;
|
||||
}
|
||||
delete vid_dumper;
|
||||
vid_dumper = NULL;
|
||||
});
|
||||
}
|
253
avidump/jmd.cpp
Normal file
253
avidump/jmd.cpp
Normal file
|
@ -0,0 +1,253 @@
|
|||
#include "jmd.hpp"
|
||||
#include <iostream>
|
||||
#include <zlib.h>
|
||||
#include <stdexcept>
|
||||
#include <cstring>
|
||||
|
||||
namespace
|
||||
{
|
||||
void write32(char* x, uint64_t v)
|
||||
{
|
||||
x[0] = (v >> 24);
|
||||
x[1] = (v >> 16);
|
||||
x[2] = (v >> 8);
|
||||
x[3] = v;
|
||||
}
|
||||
|
||||
uint32_t palette[32768];
|
||||
void init_palette()
|
||||
{
|
||||
static bool i = false;
|
||||
if(i)
|
||||
return;
|
||||
i = true;
|
||||
uint8_t rpalette[131072];
|
||||
for(unsigned j = 0; j < 32768; j++) {
|
||||
rpalette[4 * j + 0] = ((j >> 10) & 31) << 3;
|
||||
rpalette[4 * j + 1] = ((j >> 5) & 31) << 3;
|
||||
rpalette[4 * j + 2] = (j & 31) << 3;
|
||||
rpalette[4 * j + 3] = 0;
|
||||
}
|
||||
memcpy(palette, rpalette, 131072);
|
||||
}
|
||||
}
|
||||
|
||||
void jmd_dumper::video(uint64_t ts, uint16_t* memory, uint32_t width, uint32_t height)
|
||||
{
|
||||
frame_buffer f;
|
||||
f.ts = ts;
|
||||
size_t fsize = 0;
|
||||
//We'll compress the frame here.
|
||||
f.data = compress_frame(memory, width, height);
|
||||
frames.push_back(f);
|
||||
flush_buffers(false);
|
||||
}
|
||||
|
||||
void jmd_dumper::audio(uint64_t ts, short l, short r)
|
||||
{
|
||||
sample_buffer s;
|
||||
s.ts = ts;
|
||||
s.l = l;
|
||||
s.r = r;
|
||||
samples.push_back(s);
|
||||
flush_buffers(false);
|
||||
}
|
||||
|
||||
jmd_dumper::jmd_dumper(const std::string& filename, unsigned level)
|
||||
{
|
||||
clevel = level;
|
||||
jmd.open(filename.c_str(), std::ios::out | std::ios::binary);
|
||||
if(!jmd)
|
||||
throw std::runtime_error("Can't open output JMD file.");
|
||||
last_written_ts = 0;
|
||||
//Write the segment tables.
|
||||
//Stream #0 is video.
|
||||
//Stream #1 is PCM audio.
|
||||
//Stream #2 is Gameinfo.
|
||||
//Stream #3 is Dummy.
|
||||
char header[] = {
|
||||
/* Magic */
|
||||
-1, -1, 0x4A, 0x50, 0x43, 0x52, 0x52, 0x4D, 0x55, 0x4C, 0x54, 0x49, 0x44, 0x55, 0x4D, 0x50,
|
||||
/* Channel count. */
|
||||
0x00, 0x04,
|
||||
/* Video channel header. */
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 'v', 'i',
|
||||
/* Audio channel header. */
|
||||
0x00, 0x01, 0x00, 0x01, 0x00, 0x02, 'a', 'u',
|
||||
/* Gameinfo channel header. */
|
||||
0x00, 0x02, 0x00, 0x05, 0x00, 0x02, 'g', 'i',
|
||||
/* Dummy channel header. */
|
||||
0x00, 0x03, 0x00, 0x03, 0x00, 0x02, 'd', 'u'
|
||||
};
|
||||
jmd.write(header, sizeof(header));
|
||||
if(!jmd)
|
||||
throw std::runtime_error("Can't write JMD header and segment table");
|
||||
}
|
||||
|
||||
jmd_dumper::~jmd_dumper()
|
||||
{
|
||||
try {
|
||||
end(last_written_ts);
|
||||
} catch(...) {
|
||||
}
|
||||
}
|
||||
|
||||
void jmd_dumper::end(uint64_t ts)
|
||||
{
|
||||
flush_buffers(true);
|
||||
if(last_written_ts > ts) {
|
||||
jmd.close();
|
||||
return;
|
||||
}
|
||||
char dummypacket[8] = {0x00, 0x03};
|
||||
write32(dummypacket + 2, ts - last_written_ts);
|
||||
last_written_ts = ts;
|
||||
jmd.write(dummypacket, sizeof(dummypacket));
|
||||
if(!jmd)
|
||||
throw std::runtime_error("Can't write JMD ending dummy packet");
|
||||
jmd.close();
|
||||
}
|
||||
|
||||
void jmd_dumper::gameinfo(const std::string& gamename, const std::list<std::pair<std::string, std::string>>&
|
||||
authors, double gametime, const std::string& rerecords)
|
||||
{
|
||||
//FIXME: Implement this.
|
||||
}
|
||||
|
||||
void jmd_dumper::flush_buffers(bool force)
|
||||
{
|
||||
while(!frames.empty() || !samples.empty()) {
|
||||
if(frames.empty() || samples.empty()) {
|
||||
if(!force)
|
||||
return;
|
||||
else if(!frames.empty()) {
|
||||
frame_buffer& f = frames.front();
|
||||
flush_frame(f);
|
||||
frames.pop_front();
|
||||
} else if(!samples.empty()) {
|
||||
sample_buffer& s = samples.front();
|
||||
flush_sample(s);
|
||||
samples.pop_front();
|
||||
}
|
||||
continue;
|
||||
}
|
||||
frame_buffer& f = frames.front();
|
||||
sample_buffer& s = samples.front();
|
||||
if(f.ts <= s.ts) {
|
||||
flush_frame(f);
|
||||
frames.pop_front();
|
||||
} else {
|
||||
flush_sample(s);
|
||||
samples.pop_front();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void jmd_dumper::flush_frame(frame_buffer& f)
|
||||
{
|
||||
char videopacketh[16] = {0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01};
|
||||
write32(videopacketh + 2, f.ts - last_written_ts);
|
||||
last_written_ts = f.ts;
|
||||
unsigned lneed = 0;
|
||||
if(f.data.size() >= (1ULL << 63))
|
||||
videopacketh[7 + lneed++] = 0x80 | ((f.data.size() >> 63) & 0x7F);
|
||||
if(f.data.size() >= (1ULL << 56))
|
||||
videopacketh[7 + lneed++] = 0x80 | ((f.data.size() >> 56) & 0x7F);
|
||||
if(f.data.size() >= (1ULL << 49))
|
||||
videopacketh[7 + lneed++] = 0x80 | ((f.data.size() >> 49) & 0x7F);
|
||||
if(f.data.size() >= (1ULL << 42))
|
||||
videopacketh[7 + lneed++] = 0x80 | ((f.data.size() >> 42) & 0x7F);
|
||||
if(f.data.size() >= (1ULL << 35))
|
||||
videopacketh[7 + lneed++] = 0x80 | ((f.data.size() >> 35) & 0x7F);
|
||||
if(f.data.size() >= (1ULL << 28))
|
||||
videopacketh[7 + lneed++] = 0x80 | ((f.data.size() >> 28) & 0x7F);
|
||||
if(f.data.size() >= (1ULL << 21))
|
||||
videopacketh[7 + lneed++] = 0x80 | ((f.data.size() >> 21) & 0x7F);
|
||||
if(f.data.size() >= (1ULL << 14))
|
||||
videopacketh[7 + lneed++] = 0x80 | ((f.data.size() >> 14) & 0x7F);
|
||||
if(f.data.size() >= (1ULL << 7))
|
||||
videopacketh[7 + lneed++] = 0x80 | ((f.data.size() >> 7) & 0x7F);
|
||||
videopacketh[7 + lneed++] = (f.data.size() & 0x7F);
|
||||
|
||||
jmd.write(videopacketh, 7 + lneed);
|
||||
if(!jmd)
|
||||
throw std::runtime_error("Can't write JMD video packet header");
|
||||
if(f.data.size() > 0)
|
||||
jmd.write(&f.data[0], f.data.size());
|
||||
if(!jmd)
|
||||
throw std::runtime_error("Can't write JMD video packet body");
|
||||
}
|
||||
|
||||
void jmd_dumper::flush_sample(sample_buffer& s)
|
||||
{
|
||||
char soundpacket[12] = {0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x01, 0x04};
|
||||
write32(soundpacket + 2, s.ts - last_written_ts);
|
||||
last_written_ts = s.ts;
|
||||
soundpacket[8] = (s.l >> 8) & 0xFF;
|
||||
soundpacket[9] = s.l & 0xFF;
|
||||
soundpacket[10] = (s.r >> 8) & 0xFF;
|
||||
soundpacket[11] = s.r & 0xFF;
|
||||
jmd.write(soundpacket, sizeof(soundpacket));
|
||||
if(!jmd)
|
||||
throw std::runtime_error("Can't write JMD sound packet");
|
||||
}
|
||||
|
||||
#define INBUF_PIXELS 4096
|
||||
#define OUTBUF_ADVANCE 4096
|
||||
|
||||
std::vector<char> jmd_dumper::compress_frame(uint16_t* memory, uint32_t width, uint32_t height)
|
||||
{
|
||||
std::vector<char> ret;
|
||||
z_stream stream;
|
||||
memset(&stream, 0, sizeof(stream));
|
||||
if(deflateInit(&stream, clevel) != Z_OK)
|
||||
throw std::runtime_error("Can't initialize zlib stream");
|
||||
|
||||
size_t usize = 4;
|
||||
ret.resize(4);
|
||||
ret[0] = (width >> 8);
|
||||
ret[1] = width;
|
||||
ret[2] = (height >> 8);
|
||||
ret[3] = height;
|
||||
uint32_t input_buffer[INBUF_PIXELS];
|
||||
init_palette();
|
||||
size_t ptr = 0;
|
||||
size_t pixels = static_cast<size_t>(width) * height;
|
||||
bool input_clear = true;
|
||||
bool flushed = false;
|
||||
size_t bsize = 0;
|
||||
while(1) {
|
||||
|
||||
if(input_clear) {
|
||||
size_t pixel = ptr;
|
||||
for(unsigned i = 0; i < INBUF_PIXELS && pixel < pixels; i++, pixel++)
|
||||
input_buffer[i] = palette[memory[pixel]];
|
||||
bsize = pixel - ptr;
|
||||
ptr = pixel;
|
||||
input_clear = false;
|
||||
//Now the input data to compress is in input_buffer, bsize elements.
|
||||
stream.next_in = reinterpret_cast<uint8_t*>(input_buffer);
|
||||
stream.avail_in = 4 * bsize;
|
||||
}
|
||||
if(!stream.avail_out) {
|
||||
if(flushed)
|
||||
usize += (OUTBUF_ADVANCE - stream.avail_out);
|
||||
flushed = true;
|
||||
ret.resize(usize + OUTBUF_ADVANCE);
|
||||
stream.next_out = reinterpret_cast<uint8_t*>(&ret[usize]);
|
||||
stream.avail_out = OUTBUF_ADVANCE;
|
||||
}
|
||||
int r = deflate(&stream, (ptr == pixels) ? Z_FINISH : 0);
|
||||
if(r == Z_STREAM_END)
|
||||
break;
|
||||
if(r != Z_OK)
|
||||
throw std::runtime_error("Can't deflate data");
|
||||
if(!stream.avail_in)
|
||||
input_clear = true;
|
||||
}
|
||||
usize += (OUTBUF_ADVANCE - stream.avail_out);
|
||||
deflateEnd(&stream);
|
||||
|
||||
ret.resize(usize);
|
||||
return ret;
|
||||
}
|
47
avidump/jmd.hpp
Normal file
47
avidump/jmd.hpp
Normal file
|
@ -0,0 +1,47 @@
|
|||
#ifndef _jmd__hpp__included__
|
||||
#define _jmd__hpp__included__
|
||||
|
||||
#include <cstdint>
|
||||
#include <string>
|
||||
#include <fstream>
|
||||
#include <deque>
|
||||
#include <list>
|
||||
#include <vector>
|
||||
|
||||
class jmd_dumper
|
||||
{
|
||||
public:
|
||||
jmd_dumper(const std::string& filename, unsigned level);
|
||||
~jmd_dumper();
|
||||
void video(uint64_t ts, uint16_t* memory, uint32_t width, uint32_t height);
|
||||
void audio(uint64_t ts, short l, short r);
|
||||
void gameinfo(const std::string& gamename, const std::list<std::pair<std::string, std::string>>&
|
||||
authors, double gametime, const std::string& rerecords);
|
||||
void end(uint64_t ts);
|
||||
private:
|
||||
struct frame_buffer
|
||||
{
|
||||
uint64_t ts;
|
||||
std::vector<char> data;
|
||||
};
|
||||
struct sample_buffer
|
||||
{
|
||||
uint64_t ts;
|
||||
short l;
|
||||
short r;
|
||||
};
|
||||
|
||||
std::deque<frame_buffer> frames;
|
||||
std::deque<sample_buffer> samples;
|
||||
|
||||
std::vector<char> compress_frame(uint16_t* memory, uint32_t width, uint32_t height);
|
||||
void flush_buffers(bool force);
|
||||
void flush_frame(frame_buffer& f);
|
||||
void flush_sample(sample_buffer& s);
|
||||
|
||||
std::ofstream jmd;
|
||||
uint64_t last_written_ts;
|
||||
unsigned clevel;
|
||||
};
|
||||
|
||||
#endif
|
24
manual.lyx
24
manual.lyx
|
@ -871,6 +871,30 @@ end-avi
|
|||
End current AVI video dump (closing the emulator also closes the dump).
|
||||
\end_layout
|
||||
|
||||
\begin_layout Subsection
|
||||
JMD video dumping
|
||||
\end_layout
|
||||
|
||||
\begin_layout Standard
|
||||
Following commands control JMD video dumping:
|
||||
\end_layout
|
||||
|
||||
\begin_layout Subsubsection
|
||||
dump-jmd <level> <file>
|
||||
\end_layout
|
||||
|
||||
\begin_layout Standard
|
||||
Dump JMD video to file <file> at level <level> (0-9).
|
||||
\end_layout
|
||||
|
||||
\begin_layout Subsubsection
|
||||
end-jmd
|
||||
\end_layout
|
||||
|
||||
\begin_layout Standard
|
||||
End the current JMD dump in progress.
|
||||
\end_layout
|
||||
|
||||
\begin_layout Subsection
|
||||
Memory manipulation
|
||||
\end_layout
|
||||
|
|
Loading…
Add table
Reference in a new issue