From 65f5f370332a5a0686967b0a54c892be47b733ed Mon Sep 17 00:00:00 2001 From: Ilari Liusvaara Date: Wed, 12 Oct 2011 03:29:52 +0300 Subject: [PATCH] JMD dumping support --- avidump/jmd-control.cpp | 164 ++++++++++++++++++++++++++ avidump/jmd.cpp | 253 ++++++++++++++++++++++++++++++++++++++++ avidump/jmd.hpp | 47 ++++++++ manual.lyx | 24 ++++ 4 files changed, 488 insertions(+) create mode 100644 avidump/jmd-control.cpp create mode 100644 avidump/jmd.cpp create mode 100644 avidump/jmd.hpp diff --git a/avidump/jmd-control.cpp b/avidump/jmd-control.cpp new file mode 100644 index 00000000..2b11ece1 --- /dev/null +++ b/avidump/jmd-control.cpp @@ -0,0 +1,164 @@ +#include "lua.hpp" +#include "jmd.hpp" +#include "settings.hpp" +#include "misc.hpp" +#include +#include +#include +#include +#include +#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>& + 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 jmd_dump("dump-jmd", "Start JMD capture", + "Syntax: dump-jmd \nStart JMD capture to using compression\n" + "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(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; + }); +} diff --git a/avidump/jmd.cpp b/avidump/jmd.cpp new file mode 100644 index 00000000..fc8a8f1e --- /dev/null +++ b/avidump/jmd.cpp @@ -0,0 +1,253 @@ +#include "jmd.hpp" +#include +#include +#include +#include + +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>& + 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 jmd_dumper::compress_frame(uint16_t* memory, uint32_t width, uint32_t height) +{ + std::vector 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(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(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(&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; +} diff --git a/avidump/jmd.hpp b/avidump/jmd.hpp new file mode 100644 index 00000000..62aa8caa --- /dev/null +++ b/avidump/jmd.hpp @@ -0,0 +1,47 @@ +#ifndef _jmd__hpp__included__ +#define _jmd__hpp__included__ + +#include +#include +#include +#include +#include +#include + +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>& + authors, double gametime, const std::string& rerecords); + void end(uint64_t ts); +private: + struct frame_buffer + { + uint64_t ts; + std::vector data; + }; + struct sample_buffer + { + uint64_t ts; + short l; + short r; + }; + + std::deque frames; + std::deque samples; + + std::vector 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 diff --git a/manual.lyx b/manual.lyx index c08504a1..48637d38 100644 --- a/manual.lyx +++ b/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 +\end_layout + +\begin_layout Standard +Dump JMD video to file at 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