X-Git-Url: https://git.sesse.net/?a=blobdiff_plain;f=jpeg_frame_view.cpp;h=be53688a03ee33bca3bb50688030978eb411ade2;hb=bdef311c334b674ba39a931805fb7d32ce8698da;hp=8e22f629541903fc2b2d20d1399aa18c33ec57fd;hpb=530f3b3aa10aafaf22da10e53d3d243df26bf174;p=nageru diff --git a/jpeg_frame_view.cpp b/jpeg_frame_view.cpp index 8e22f62..be53688 100644 --- a/jpeg_frame_view.cpp +++ b/jpeg_frame_view.cpp @@ -1,30 +1,55 @@ #include "jpeg_frame_view.h" -#include -#include +#include "defs.h" +#include "jpeg_destroyer.h" +#include "post_to_main_thread.h" +#include "video_stream.h" +#include "ycbcr_converter.h" +#include +#include #include #include #include +#include +#include +#include +#include #include +#include #include +#include #include -#include - -#include -#include -#include - -#include "defs.h" -#include "post_to_main_thread.h" -#include "video_stream.h" +// Must come after the Qt stuff. +#include "vaapi_jpeg_decoder.h" using namespace movit; using namespace std; -bool operator< (const JPEGID &a, const JPEGID &b) { - return make_pair(a.stream_idx, a.pts) < make_pair(b.stream_idx, b.pts); +namespace { + +// Just an arbitrary order for std::map. +struct FrameOnDiskLexicalOrder +{ + bool operator() (const FrameOnDisk &a, const FrameOnDisk &b) const + { + if (a.pts != b.pts) + return a.pts < b.pts; + if (a.offset != b.offset) + return a.offset < b.offset; + if (a.filename_idx != b.filename_idx) + return a.filename_idx < b.filename_idx; + assert(a.size == b.size); + return false; + } +}; + +inline size_t frame_size(const Frame &frame) +{ + size_t y_size = frame.width * frame.height; + size_t cbcr_size = y_size / frame.chroma_subsampling_x / frame.chroma_subsampling_y; + return y_size + cbcr_size * 2; } struct LRUFrame { @@ -32,22 +57,49 @@ struct LRUFrame { size_t last_used; }; +struct PendingDecode { + JPEGFrameView *destination; + + // For actual decodes (only if frame below is nullptr). + FrameOnDisk primary, secondary; + float fade_alpha; // Irrelevant if secondary.stream_idx == -1. + + // Already-decoded frames are also sent through PendingDecode, + // so that they get drawn in the right order. If frame is nullptr, + // it's a real decode. + shared_ptr frame; +}; + +} // namespace + +thread JPEGFrameView::jpeg_decoder_thread; mutex cache_mu; -map cache; // Under cache_mu. +map cache; // Under cache_mu. +size_t cache_bytes_used = 0; // Under cache_mu. condition_variable any_pending_decodes; -deque> pending_decodes; // Under cache_mu. +deque pending_decodes; // Under cache_mu. atomic event_counter{0}; extern QGLWidget *global_share_widget; +extern atomic should_quit; -// TODO: Decode using VA-API if available. shared_ptr decode_jpeg(const string &filename) { - shared_ptr frame(new Frame); + shared_ptr frame; + if (vaapi_jpeg_decoding_usable) { + frame = decode_jpeg_vaapi(filename); + if (frame != nullptr) { + return frame; + } + fprintf(stderr, "VA-API hardware decoding failed; falling back to software.\n"); + } + + frame.reset(new Frame); jpeg_decompress_struct dinfo; jpeg_error_mgr jerr; dinfo.err = jpeg_std_error(&jerr); jpeg_create_decompress(&dinfo); + JPEGDestroyer destroy_dinfo(&dinfo); FILE *fp = fopen(filename.c_str(), "rb"); if (fp == nullptr) { @@ -117,8 +169,7 @@ shared_ptr decode_jpeg(const string &filename) jpeg_read_raw_data(&dinfo, data, v_mcu_size); } - (void) jpeg_finish_decompress(&dinfo); - jpeg_destroy_decompress(&dinfo); + (void)jpeg_finish_decompress(&dinfo); fclose(fp); return frame; @@ -127,16 +178,28 @@ shared_ptr decode_jpeg(const string &filename) void prune_cache() { // Assumes cache_mu is held. - vector lru_timestamps; + int64_t bytes_still_to_remove = cache_bytes_used - (size_t(CACHE_SIZE_MB) * 1024 * 1024) * 9 / 10; + if (bytes_still_to_remove <= 0) return; + + vector> lru_timestamps_and_size; for (const auto &key_and_value : cache) { - lru_timestamps.push_back(key_and_value.second.last_used); + lru_timestamps_and_size.emplace_back( + key_and_value.second.last_used, + frame_size(*key_and_value.second.frame)); + } + sort(lru_timestamps_and_size.begin(), lru_timestamps_and_size.end()); + + // Remove the oldest ones until we are below 90% of the cache used. + size_t lru_cutoff_point = 0; + for (const pair &it : lru_timestamps_and_size) { + lru_cutoff_point = it.first; + bytes_still_to_remove -= it.second; + if (bytes_still_to_remove <= 0) break; } - size_t cutoff_point = CACHE_SIZE / 10; // Prune away the 10% oldest ones. - nth_element(lru_timestamps.begin(), lru_timestamps.begin() + cutoff_point, lru_timestamps.end()); - size_t must_be_used_after = lru_timestamps[cutoff_point]; for (auto it = cache.begin(); it != cache.end(); ) { - if (it->second.last_used < must_be_used_after) { + if (it->second.last_used <= lru_cutoff_point) { + cache_bytes_used -= frame_size(*it->second.frame); it = cache.erase(it); } else { ++it; @@ -144,12 +207,12 @@ void prune_cache() } } -shared_ptr decode_jpeg_with_cache(JPEGID id, CacheMissBehavior cache_miss_behavior, bool *did_decode) +shared_ptr decode_jpeg_with_cache(FrameOnDisk frame_spec, CacheMissBehavior cache_miss_behavior, bool *did_decode) { *did_decode = false; { unique_lock lock(cache_mu); - auto it = cache.find(id); + auto it = cache.find(frame_spec); if (it != cache.end()) { it->second.last_used = event_counter++; return it->second.frame; @@ -161,38 +224,39 @@ shared_ptr decode_jpeg_with_cache(JPEGID id, CacheMissBehavior cache_miss } *did_decode = true; - shared_ptr frame = decode_jpeg(filename_for_frame(id.stream_idx, id.pts)); + shared_ptr frame = decode_jpeg(read_frame(frame_spec)); unique_lock lock(cache_mu); - cache[id] = LRUFrame{ frame, event_counter++ }; + cache_bytes_used += frame_size(*frame); + cache[frame_spec] = LRUFrame{ frame, event_counter++ }; - if (cache.size() > CACHE_SIZE) { + if (cache_bytes_used > size_t(CACHE_SIZE_MB) * 1024 * 1024) { prune_cache(); } return frame; } -void jpeg_decoder_thread() +void jpeg_decoder_thread_func() { size_t num_decoded = 0, num_dropped = 0; pthread_setname_np(pthread_self(), "JPEGDecoder"); - for ( ;; ) { - JPEGID id; - JPEGFrameView *dest; + while (!should_quit.load()) { + PendingDecode decode; CacheMissBehavior cache_miss_behavior = DECODE_IF_NOT_IN_CACHE; { unique_lock lock(cache_mu); // TODO: Perhaps under another lock? any_pending_decodes.wait(lock, [] { - return !pending_decodes.empty(); + return !pending_decodes.empty() || should_quit.load(); }); - id = pending_decodes.front().first; - dest = pending_decodes.front().second; + if (should_quit.load()) + break; + decode = pending_decodes.front(); pending_decodes.pop_front(); size_t num_pending = 0; - for (const pair &decode : pending_decodes) { - if (decode.second == dest) { + for (const PendingDecode &other_decode : pending_decodes) { + if (other_decode.destination == decode.destination) { ++num_pending; } } @@ -201,35 +265,85 @@ void jpeg_decoder_thread() } } - bool found_in_cache; - shared_ptr frame = decode_jpeg_with_cache(id, cache_miss_behavior, &found_in_cache); - - if (frame == nullptr) { - assert(cache_miss_behavior == RETURN_NULLPTR_IF_NOT_IN_CACHE); - ++num_dropped; + if (decode.frame != nullptr) { + // Already decoded, so just show it. + decode.destination->setDecodedFrame(decode.frame, nullptr, 1.0f); continue; } - if (!found_in_cache) { - ++num_decoded; - if (num_decoded % 1000 == 0) { - fprintf(stderr, "Decoded %zu images, dropped %zu (%.2f%% dropped)\n", - num_decoded, num_dropped, (100.0 * num_dropped) / (num_decoded + num_dropped)); + shared_ptr primary_frame, secondary_frame; + bool drop = false; + for (int subframe_idx = 0; subframe_idx < 2; ++subframe_idx) { + const FrameOnDisk &frame_spec = (subframe_idx == 0 ? decode.primary : decode.secondary); + if (frame_spec.pts == -1) { + // No secondary frame. + continue; + } + + bool found_in_cache; + shared_ptr frame = decode_jpeg_with_cache(frame_spec, cache_miss_behavior, &found_in_cache); + + if (frame == nullptr) { + assert(cache_miss_behavior == RETURN_NULLPTR_IF_NOT_IN_CACHE); + drop = true; + break; + } + + if (!found_in_cache) { + ++num_decoded; + if (num_decoded % 1000 == 0) { + fprintf(stderr, "Decoded %zu images, dropped %zu (%.2f%% dropped)\n", + num_decoded, num_dropped, (100.0 * num_dropped) / (num_decoded + num_dropped)); + } + } + if (subframe_idx == 0) { + primary_frame = move(frame); + } else { + secondary_frame = move(frame); } } + if (drop) { + ++num_dropped; + continue; + } - dest->setDecodedFrame(frame); + // TODO: Could we get jitter between non-interpolated and interpolated frames here? + decode.destination->setDecodedFrame(primary_frame, secondary_frame, decode.fade_alpha); } } +void JPEGFrameView::shutdown() +{ + any_pending_decodes.notify_all(); + jpeg_decoder_thread.join(); +} + JPEGFrameView::JPEGFrameView(QWidget *parent) - : QGLWidget(parent, global_share_widget) { + : QGLWidget(parent, global_share_widget) +{ } -void JPEGFrameView::update_frame() +void JPEGFrameView::setFrame(unsigned stream_idx, FrameOnDisk frame, FrameOnDisk secondary_frame, float fade_alpha) { + current_stream_idx = stream_idx; // TODO: Does this interact with fades? + unique_lock lock(cache_mu); - pending_decodes.emplace_back(JPEGID{ stream_idx, pts }, this); + PendingDecode decode; + decode.primary = frame; + decode.secondary = secondary_frame; + decode.fade_alpha = fade_alpha; + decode.destination = this; + pending_decodes.push_back(decode); + any_pending_decodes.notify_all(); +} + +void JPEGFrameView::setFrame(shared_ptr frame) +{ + unique_lock lock(cache_mu); + PendingDecode decode; + decode.frame = std::move(frame); + decode.destination = this; + pending_decodes.push_back(decode); any_pending_decodes.notify_all(); } @@ -244,35 +358,20 @@ void JPEGFrameView::initializeGL() static once_flag once; call_once(once, [] { resource_pool = new ResourcePool; - std::thread(&jpeg_decoder_thread).detach(); + jpeg_decoder_thread = std::thread(jpeg_decoder_thread_func); }); - chain.reset(new EffectChain(1280, 720, resource_pool)); - ImageFormat image_format; - image_format.color_space = COLORSPACE_sRGB; - image_format.gamma_curve = GAMMA_sRGB; - ycbcr_format.luma_coefficients = YCBCR_REC_709; - ycbcr_format.full_range = false; - ycbcr_format.num_levels = 256; - ycbcr_format.chroma_subsampling_x = 2; - ycbcr_format.chroma_subsampling_y = 1; - ycbcr_format.cb_x_position = 0.0f; // H.264 -- _not_ JPEG, even though our input is MJPEG-encoded - ycbcr_format.cb_y_position = 0.5f; // Irrelevant. - ycbcr_format.cr_x_position = 0.0f; - ycbcr_format.cr_y_position = 0.5f; - ycbcr_input = (movit::YCbCrInput *)chain->add_input(new YCbCrInput(image_format, ycbcr_format, 1280, 720)); + ycbcr_converter.reset(new YCbCrConverter(YCbCrConverter::OUTPUT_TO_RGBA, resource_pool)); ImageFormat inout_format; - inout_format.color_space = COLORSPACE_sRGB; - inout_format.gamma_curve = GAMMA_sRGB; + inout_format.color_space = COLORSPACE_sRGB; + inout_format.gamma_curve = GAMMA_sRGB; - check_error(); - chain->add_output(inout_format, OUTPUT_ALPHA_FORMAT_POSTMULTIPLIED); - check_error(); - chain->set_dither_bits(8); - check_error(); - chain->finalize(); - check_error(); + overlay_chain.reset(new EffectChain(overlay_base_width, overlay_base_height, resource_pool)); + overlay_input = (movit::FlatInput *)overlay_chain->add_input(new FlatInput(inout_format, FORMAT_GRAYSCALE, GL_UNSIGNED_BYTE, overlay_base_width, overlay_base_height)); + + overlay_chain->add_output(inout_format, OUTPUT_ALPHA_FORMAT_POSTMULTIPLIED); + overlay_chain->finalize(); } void JPEGFrameView::resizeGL(int width, int height) @@ -280,10 +379,15 @@ void JPEGFrameView::resizeGL(int width, int height) check_error(); glViewport(0, 0, width, height); check_error(); + + // Save these, as width() and height() will lie with DPI scaling. + gl_width = width; + gl_height = height; } void JPEGFrameView::paintGL() { + glViewport(0, 0, gl_width, gl_height); if (current_frame == nullptr) { glClearColor(0.0f, 0.0f, 0.0f, 1.0f); glClear(GL_COLOR_BUFFER_BIT); @@ -291,24 +395,34 @@ void JPEGFrameView::paintGL() } check_error(); - chain->render_to_screen(); + current_chain->render_to_screen(); + + if (overlay_image != nullptr) { + if (overlay_input_needs_refresh) { + overlay_input->set_width(overlay_width); + overlay_input->set_height(overlay_height); + overlay_input->set_pixel_data(overlay_image->bits()); + } + glViewport(gl_width - overlay_width, 0, overlay_width, overlay_height); + overlay_chain->render_to_screen(); + } } -void JPEGFrameView::setDecodedFrame(std::shared_ptr frame) +namespace { + +} // namespace + +void JPEGFrameView::setDecodedFrame(shared_ptr frame, shared_ptr secondary_frame, float fade_alpha) { - post_to_main_thread([this, frame] { + post_to_main_thread([this, frame, secondary_frame, fade_alpha] { current_frame = frame; - ycbcr_format.chroma_subsampling_x = frame->chroma_subsampling_x; - ycbcr_format.chroma_subsampling_y = frame->chroma_subsampling_y; - ycbcr_input->change_ycbcr_format(ycbcr_format); - ycbcr_input->set_width(frame->width); - ycbcr_input->set_height(frame->height); - ycbcr_input->set_pixel_data(0, frame->y.get()); - ycbcr_input->set_pixel_data(1, frame->cb.get()); - ycbcr_input->set_pixel_data(2, frame->cr.get()); - ycbcr_input->set_pitch(0, frame->pitch_y); - ycbcr_input->set_pitch(1, frame->pitch_chroma); - ycbcr_input->set_pitch(2, frame->pitch_chroma); + current_secondary_frame = secondary_frame; + + if (secondary_frame != nullptr) { + current_chain = ycbcr_converter->prepare_chain_for_fade(frame, secondary_frame, fade_alpha); + } else { + current_chain = ycbcr_converter->prepare_chain_for_conversion(frame); + } update(); }); } @@ -319,3 +433,30 @@ void JPEGFrameView::mousePressEvent(QMouseEvent *event) emit clicked(); } } + +void JPEGFrameView::set_overlay(const string &text) +{ + if (text.empty()) { + overlay_image.reset(); + return; + } + + float dpr = QGuiApplication::primaryScreen()->devicePixelRatio(); + overlay_width = lrint(overlay_base_width * dpr); + overlay_height = lrint(overlay_base_height * dpr); + + overlay_image.reset(new QImage(overlay_width, overlay_height, QImage::Format_Grayscale8)); + overlay_image->setDevicePixelRatio(dpr); + overlay_image->fill(0); + QPainter painter(overlay_image.get()); + + painter.setPen(Qt::white); + QFont font = painter.font(); + font.setPointSize(12); + painter.setFont(font); + + painter.drawText(QRectF(0, 0, overlay_base_width, overlay_base_height), Qt::AlignCenter, QString::fromStdString(text)); + + // Don't refresh immediately; we might not have an OpenGL context here. + overlay_input_needs_refresh = true; +}