]> git.sesse.net Git - nageru/blobdiff - mixer.cpp
Document the mlockall() change in NEWS.
[nageru] / mixer.cpp
index 8c95f386f5f4d94d929d4496be31b6a3f6950a9a..80b0df1ea0df60d852bc530cb0d5535e99e2f92a 100644 (file)
--- a/mixer.cpp
+++ b/mixer.cpp
@@ -33,7 +33,7 @@
 #include "decklink_capture.h"
 #include "defs.h"
 #include "flags.h"
-#include "h264encode.h"
+#include "video_encoder.h"
 #include "pbo_frame_allocator.h"
 #include "ref_counted_gl_sync.h"
 #include "timebase.h"
@@ -92,23 +92,6 @@ void insert_new_frame(RefCountedFrame frame, unsigned field_num, bool interlaced
        }
 }
 
-string generate_local_dump_filename(int frame)
-{
-       time_t now = time(NULL);
-       tm now_tm;
-       localtime_r(&now, &now_tm);
-
-       char timestamp[256];
-       strftime(timestamp, sizeof(timestamp), "%F-%T%z", &now_tm);
-
-       // Use the frame number to disambiguate between two cuts starting
-       // on the same second.
-       char filename[256];
-       snprintf(filename, sizeof(filename), "%s%s-f%02d%s",
-               LOCAL_DUMP_PREFIX, timestamp, frame % 100, LOCAL_DUMP_SUFFIX);
-       return filename;
-}
-
 }  // namespace
 
 void QueueLengthPolicy::update_policy(int queue_length)
@@ -127,9 +110,9 @@ void QueueLengthPolicy::update_policy(int queue_length)
                if (queue_length >= int(safe_queue_length)) {
                        been_at_safe_point_since_last_starvation = true;
                }
-               if (++frames_with_at_least_one >= 50 && safe_queue_length > 0) {
+               if (++frames_with_at_least_one >= 1000 && safe_queue_length > 0) {
                        --safe_queue_length;
-                       fprintf(stderr, "Card %u: Spare frames for more than 50 frames, reducing safe limit to %u frames\n",
+                       fprintf(stderr, "Card %u: Spare frames for more than 1000 frames, reducing safe limit to %u frames\n",
                                card_index, safe_queue_length);
                        frames_with_at_least_one = 0;
                }
@@ -159,6 +142,7 @@ Mixer::Mixer(const QSurfaceFormat &format, unsigned num_cards)
        theme.reset(new Theme(global_flags.theme_filename.c_str(), resource_pool.get(), num_cards));
        for (unsigned i = 0; i < NUM_OUTPUTS; ++i) {
                output_channel[i].parent = this;
+               output_channel[i].channel = i;
        }
 
        ImageFormat inout_format;
@@ -174,10 +158,9 @@ Mixer::Mixer(const QSurfaceFormat &format, unsigned num_cards)
        display_chain->set_dither_bits(0);  // Don't bother.
        display_chain->finalize();
 
-       h264_encoder.reset(new H264Encoder(h264_encoder_surface, global_flags.va_display, WIDTH, HEIGHT, &httpd));
-       h264_encoder->open_output_file(generate_local_dump_filename(/*frame=*/0).c_str());
+       video_encoder.reset(new VideoEncoder(resource_pool.get(), h264_encoder_surface, global_flags.va_display, WIDTH, HEIGHT, &httpd));
 
-       // Start listening for clients only once H264Encoder has written its header, if any.
+       // Start listening for clients only once VideoEncoder has written its header, if any.
        httpd.start(9095);
 
        // First try initializing the PCI devices, then USB, until we have the desired number of cards.
@@ -274,7 +257,9 @@ Mixer::Mixer(const QSurfaceFormat &format, unsigned num_cards)
        // and there's a limit to how important the peak meter is.
        peak_resampler.setup(OUTPUT_FREQUENCY, OUTPUT_FREQUENCY * 4, /*num_channels=*/2, /*hlen=*/16, /*frel=*/1.0);
 
-       alsa.reset(new ALSAOutput(OUTPUT_FREQUENCY, /*num_channels=*/2));
+       if (global_flags.enable_alsa_output) {
+               alsa.reset(new ALSAOutput(OUTPUT_FREQUENCY, /*num_channels=*/2));
+       }
 }
 
 Mixer::~Mixer()
@@ -292,7 +277,7 @@ Mixer::~Mixer()
                cards[card_index].capture->stop_dequeue_thread();
        }
 
-       h264_encoder.reset(nullptr);
+       video_encoder.reset(nullptr);
 }
 
 void Mixer::configure_card(unsigned card_index, const QSurfaceFormat &format, CaptureInterface *capture)
@@ -305,19 +290,7 @@ void Mixer::configure_card(unsigned card_index, const QSurfaceFormat &format, Ca
        card->frame_allocator.reset(new PBOFrameAllocator(8 << 20, WIDTH, HEIGHT));  // 8 MB.
        card->capture->set_video_frame_allocator(card->frame_allocator.get());
        card->surface = create_surface(format);
-       card->capture->set_dequeue_thread_callbacks(
-               [card]{
-                       eglBindAPI(EGL_OPENGL_API);
-                       card->context = create_context(card->surface);
-                       if (!make_current(card->context, card->surface)) {
-                               printf("failed to create bmusb context\n");
-                               exit(1);
-                       }
-               },
-               [this]{
-                       resource_pool->clean_context();
-               });
-       card->resampling_queue.reset(new ResamplingQueue(OUTPUT_FREQUENCY, OUTPUT_FREQUENCY, 2));
+       card->resampling_queue.reset(new ResamplingQueue(card_index, OUTPUT_FREQUENCY, OUTPUT_FREQUENCY, 2));
        card->capture->configure_card();
 }
 
@@ -386,7 +359,7 @@ void Mixer::bm_frame(unsigned card_index, uint16_t timecode,
                }
        }
 
-       int64_t frame_length = int64_t(TIMEBASE * video_format.frame_rate_den) / video_format.frame_rate_nom;
+       int64_t frame_length = int64_t(TIMEBASE) * video_format.frame_rate_den / video_format.frame_rate_nom;
 
        size_t num_samples = (audio_frame.len > audio_offset) ? (audio_frame.len - audio_offset) / audio_format.num_channels / (audio_format.bits_per_sample / 8) : 0;
        if (num_samples > OUTPUT_FREQUENCY / 10) {
@@ -437,7 +410,7 @@ void Mixer::bm_frame(unsigned card_index, uint16_t timecode,
                if (dropped_frames > MAX_FPS * 2) {
                        fprintf(stderr, "Card %d lost more than two seconds (or time code jumping around; from 0x%04x to 0x%04x), resetting resampler\n",
                                card_index, card->last_timecode, timecode);
-                       card->resampling_queue.reset(new ResamplingQueue(OUTPUT_FREQUENCY, OUTPUT_FREQUENCY, 2));
+                       card->resampling_queue.reset(new ResamplingQueue(card_index, OUTPUT_FREQUENCY, OUTPUT_FREQUENCY, 2));
                        dropped_frames = 0;
                } else if (dropped_frames > 0) {
                        // Insert silence as needed.
@@ -519,59 +492,64 @@ void Mixer::bm_frame(unsigned card_index, uint16_t timecode,
        size_t y_offset = video_frame.size / 2 + video_offset / 2;
 
        for (unsigned field = 0; field < num_fields; ++field) {
-               unsigned field_start_line = (field == 1) ? video_format.second_field_start : video_format.extra_lines_top + field * (video_format.height + 22);
-
-               if (userdata->tex_y[field] == 0 ||
-                   userdata->tex_cbcr[field] == 0 ||
-                   video_format.width != userdata->last_width[field] ||
-                   video_format.height != userdata->last_height[field]) {
-                       // We changed resolution since last use of this texture, so we need to create
-                       // a new object. Note that this each card has its own PBOFrameAllocator,
-                       // we don't need to worry about these flip-flopping between resolutions.
+               // Put the actual texture upload in a lambda that is executed in the main thread.
+               // It is entirely possible to do this in the same thread (and it might even be
+               // faster, depending on the GPU and driver), but it appears to be trickling
+               // driver bugs very easily.
+               //
+               // Note that this means we must hold on to the actual frame data in <userdata>
+               // until the upload command is run, but we hold on to <frame> much longer than that
+               // (in fact, all the way until we no longer use the texture in rendering).
+               auto upload_func = [field, video_format, y_offset, cbcr_offset, cbcr_width, userdata]() {
+                       unsigned field_start_line = (field == 1) ? video_format.second_field_start : video_format.extra_lines_top + field * (video_format.height + 22);
+
+                       if (userdata->tex_y[field] == 0 ||
+                           userdata->tex_cbcr[field] == 0 ||
+                           video_format.width != userdata->last_width[field] ||
+                           video_format.height != userdata->last_height[field]) {
+                               // We changed resolution since last use of this texture, so we need to create
+                               // a new object. Note that this each card has its own PBOFrameAllocator,
+                               // we don't need to worry about these flip-flopping between resolutions.
+                               glBindTexture(GL_TEXTURE_2D, userdata->tex_cbcr[field]);
+                               check_error();
+                               glTexImage2D(GL_TEXTURE_2D, 0, GL_RG8, cbcr_width, video_format.height, 0, GL_RG, GL_UNSIGNED_BYTE, nullptr);
+                               check_error();
+                               glBindTexture(GL_TEXTURE_2D, userdata->tex_y[field]);
+                               check_error();
+                               glTexImage2D(GL_TEXTURE_2D, 0, GL_R8, video_format.width, video_format.height, 0, GL_RED, GL_UNSIGNED_BYTE, nullptr);
+                               check_error();
+                               userdata->last_width[field] = video_format.width;
+                               userdata->last_height[field] = video_format.height;
+                       }
+
+                       GLuint pbo = userdata->pbo;
+                       check_error();
+                       glBindBuffer(GL_PIXEL_UNPACK_BUFFER, pbo);
+                       check_error();
+
+                       size_t field_y_start = y_offset + video_format.width * field_start_line;
+                       size_t field_cbcr_start = cbcr_offset + cbcr_width * field_start_line * sizeof(uint16_t);
+
+                       if (global_flags.flush_pbos) {
+                               glFlushMappedBufferRange(GL_PIXEL_UNPACK_BUFFER, field_y_start, video_format.width * video_format.height);
+                               check_error();
+                               glFlushMappedBufferRange(GL_PIXEL_UNPACK_BUFFER, field_cbcr_start, cbcr_width * video_format.height * sizeof(uint16_t));
+                               check_error();
+                       }
+
                        glBindTexture(GL_TEXTURE_2D, userdata->tex_cbcr[field]);
                        check_error();
-                       glTexImage2D(GL_TEXTURE_2D, 0, GL_RG8, cbcr_width, video_format.height, 0, GL_RG, GL_UNSIGNED_BYTE, nullptr);
+                       glTexSubImage2D(GL_TEXTURE_2D, 0, 0, 0, cbcr_width, video_format.height, GL_RG, GL_UNSIGNED_BYTE, BUFFER_OFFSET(field_cbcr_start));
                        check_error();
                        glBindTexture(GL_TEXTURE_2D, userdata->tex_y[field]);
                        check_error();
-                       glTexImage2D(GL_TEXTURE_2D, 0, GL_R8, video_format.width, video_format.height, 0, GL_RED, GL_UNSIGNED_BYTE, nullptr);
+                       glTexSubImage2D(GL_TEXTURE_2D, 0, 0, 0, video_format.width, video_format.height, GL_RED, GL_UNSIGNED_BYTE, BUFFER_OFFSET(field_y_start));
                        check_error();
-                       userdata->last_width[field] = video_format.width;
-                       userdata->last_height[field] = video_format.height;
-               }
-
-               GLuint pbo = userdata->pbo;
-               check_error();
-               glBindBuffer(GL_PIXEL_UNPACK_BUFFER, pbo);
-               check_error();
-
-               size_t field_y_start = y_offset + video_format.width * field_start_line;
-               size_t field_cbcr_start = cbcr_offset + cbcr_width * field_start_line * sizeof(uint16_t);
-
-               if (global_flags.flush_pbos) {
-                       glFlushMappedBufferRange(GL_PIXEL_UNPACK_BUFFER, field_y_start, video_format.width * video_format.height);
+                       glBindTexture(GL_TEXTURE_2D, 0);
                        check_error();
-                       glFlushMappedBufferRange(GL_PIXEL_UNPACK_BUFFER, field_cbcr_start, cbcr_width * video_format.height * sizeof(uint16_t));
+                       glBindBuffer(GL_PIXEL_UNPACK_BUFFER, 0);
                        check_error();
-               }
-
-               glBindTexture(GL_TEXTURE_2D, userdata->tex_cbcr[field]);
-               check_error();
-               glTexSubImage2D(GL_TEXTURE_2D, 0, 0, 0, cbcr_width, video_format.height, GL_RG, GL_UNSIGNED_BYTE, BUFFER_OFFSET(field_cbcr_start));
-               check_error();
-               glBindTexture(GL_TEXTURE_2D, userdata->tex_y[field]);
-               check_error();
-               glTexSubImage2D(GL_TEXTURE_2D, 0, 0, 0, video_format.width, video_format.height, GL_RED, GL_UNSIGNED_BYTE, BUFFER_OFFSET(field_y_start));
-               check_error();
-               glBindTexture(GL_TEXTURE_2D, 0);
-               check_error();
-               glBindBuffer(GL_PIXEL_UNPACK_BUFFER, 0);
-               check_error();
-               RefCountedGLsync fence(GL_SYNC_GPU_COMMANDS_COMPLETE, /*flags=*/0);
-               check_error();
-               assert(fence.get() != nullptr);
-               glFlush();  // Make sure the main thread doesn't have to wait until we push out enough frames to make a new command buffer.
-               check_error();
+               };
 
                if (field == 1) {
                        // Don't upload the second field as fast as we can; wait until
@@ -599,7 +577,7 @@ void Mixer::bm_frame(unsigned card_index, uint16_t timecode,
                        new_frame.length = frame_length;
                        new_frame.field = field;
                        new_frame.interlaced = video_format.interlaced;
-                       new_frame.ready_fence = fence;
+                       new_frame.upload_func = upload_func;
                        new_frame.dropped_frames = dropped_frames;
                        card->new_frames.push(move(new_frame));
                        card->new_frames_changed.notify_all();
@@ -666,19 +644,17 @@ void Mixer::thread_func()
                        insert_new_frame(new_frame->frame, new_frame->field, new_frame->interlaced, card_index, &input_state);
                        check_error();
 
-                       // The new texture might still be uploaded,
-                       // tell the GPU to wait until it's there.
-                       if (new_frame->ready_fence) {
-                               glWaitSync(new_frame->ready_fence.get(), /*flags=*/0, GL_TIMEOUT_IGNORED);
-                               check_error();
-                               new_frame->ready_fence.reset();
-                               check_error();
+                       // The new texture might need uploading before use.
+                       if (new_frame->upload_func) {
+                               new_frame->upload_func();
+                               new_frame->upload_func = nullptr;
                        }
                }
 
-               render_one_frame();
+               int64_t duration = new_frames[master_card_index].length;
+               render_one_frame(duration);
                ++frame;
-               pts_int += new_frames[master_card_index].length;
+               pts_int += duration;
 
                clock_gettime(CLOCK_MONOTONIC, &now);
                double elapsed = now.tv_sec - start.tv_sec +
@@ -691,12 +667,7 @@ void Mixer::thread_func()
                }
 
                if (should_cut.exchange(false)) {  // Test and clear.
-                       string filename = generate_local_dump_filename(frame);
-                       printf("Starting new recording: %s\n", filename.c_str());
-                       h264_encoder->close_output_file();
-                       h264_encoder->shutdown();
-                       h264_encoder.reset(new H264Encoder(h264_encoder_surface, global_flags.va_display, WIDTH, HEIGHT, &httpd));
-                       h264_encoder->open_output_file(filename.c_str());
+                       video_encoder->do_cut(frame);
                }
 
 #if 0
@@ -774,7 +745,7 @@ void Mixer::schedule_audio_resampling_tasks(unsigned dropped_frames, int num_sam
        }
 }
 
-void Mixer::render_one_frame()
+void Mixer::render_one_frame(int64_t duration)
 {
        // Get the main chain from the theme, and set its state immediately.
        Theme::Chain theme_main_chain = theme->get_chain(0, pts(), WIDTH, HEIGHT, input_state);
@@ -783,7 +754,7 @@ void Mixer::render_one_frame()
        //theme_main_chain.chain->enable_phase_timing(true);
 
        GLuint y_tex, cbcr_tex;
-       bool got_frame = h264_encoder->begin_frame(&y_tex, &cbcr_tex);
+       bool got_frame = video_encoder->begin_frame(&y_tex, &cbcr_tex);
        assert(got_frame);
 
        // Render main chain.
@@ -805,7 +776,7 @@ void Mixer::render_one_frame()
        glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);
 
        const int64_t av_delay = TIMEBASE / 10;  // Corresponds to the fixed delay in resampling_queue.h. TODO: Make less hard-coded.
-       RefCountedGLsync fence = h264_encoder->end_frame(pts_int + av_delay, theme_main_chain.input_frames);
+       RefCountedGLsync fence = video_encoder->end_frame(pts_int + av_delay, duration, theme_main_chain.input_frames);
 
        // The live frame just shows the RGBA texture we just rendered.
        // It owns rgba_tex now.
@@ -857,7 +828,10 @@ void Mixer::audio_thread_func()
 
                {
                        unique_lock<mutex> lock(audio_mutex);
-                       audio_task_queue_changed.wait(lock, [this]{ return !audio_task_queue.empty(); });
+                       audio_task_queue_changed.wait(lock, [this]{ return should_quit || !audio_task_queue.empty(); });
+                       if (should_quit) {
+                               return;
+                       }
                        task = audio_task_queue.front();
                        audio_task_queue.pop();
                }
@@ -879,9 +853,7 @@ void Mixer::process_audio_one_frame(int64_t frame_pts_int, int num_samples)
                samples_card.resize(num_samples * 2);
                {
                        unique_lock<mutex> lock(cards[card_index].audio_mutex);
-                       if (!cards[card_index].resampling_queue->get_output_samples(double(frame_pts_int) / TIMEBASE, &samples_card[0], num_samples)) {
-                               printf("Card %d reported previous underrun.\n", card_index);
-                       }
+                       cards[card_index].resampling_queue->get_output_samples(double(frame_pts_int) / TIMEBASE, &samples_card[0], num_samples);
                }
                if (card_index == selected_audio_card) {
                        samples_out = move(samples_card);
@@ -1026,7 +998,7 @@ void Mixer::process_audio_one_frame(int64_t frame_pts_int, int num_samples)
        }
 
        // And finally add them to the output.
-       h264_encoder->add_audio(frame_pts_int, move(samples_out));
+       video_encoder->add_audio(frame_pts_int, move(samples_out));
 }
 
 void Mixer::subsample_chroma(GLuint src_tex, GLuint dst_tex)
@@ -1107,6 +1079,7 @@ void Mixer::start()
 void Mixer::quit()
 {
        should_quit = true;
+       audio_task_queue_changed.notify_one();
        mixer_thread.join();
        audio_thread.join();
 }
@@ -1170,9 +1143,47 @@ void Mixer::OutputChannel::output_frame(DisplayFrame frame)
                has_ready_frame = true;
        }
 
-       if (has_new_frame_ready_callback) {
+       if (new_frame_ready_callback) {
                new_frame_ready_callback();
        }
+
+       // Reduce the number of callbacks by filtering duplicates. The reason
+       // why we bother doing this is that Qt seemingly can get into a state
+       // where its builds up an essentially unbounded queue of signals,
+       // consuming more and more memory, and there's no good way of collapsing
+       // user-defined signals or limiting the length of the queue.
+       if (transition_names_updated_callback) {
+               vector<string> transition_names = global_mixer->get_transition_names();
+               bool changed = false;
+               if (transition_names.size() != last_transition_names.size()) {
+                       changed = true;
+               } else {
+                       for (unsigned i = 0; i < transition_names.size(); ++i) {
+                               if (transition_names[i] != last_transition_names[i]) {
+                                       changed = true;
+                                       break;
+                               }
+                       }
+               }
+               if (changed) {
+                       transition_names_updated_callback(transition_names);
+                       last_transition_names = transition_names;
+               }
+       }
+       if (name_updated_callback) {
+               string name = global_mixer->get_channel_name(channel);
+               if (name != last_name) {
+                       name_updated_callback(name);
+                       last_name = name;
+               }
+       }
+       if (color_updated_callback) {
+               string color = global_mixer->get_channel_color(channel);
+               if (color != last_color) {
+                       color_updated_callback(color);
+                       last_color = color;
+               }
+       }
 }
 
 bool Mixer::OutputChannel::get_display_frame(DisplayFrame *frame)
@@ -1203,5 +1214,21 @@ bool Mixer::OutputChannel::get_display_frame(DisplayFrame *frame)
 void Mixer::OutputChannel::set_frame_ready_callback(Mixer::new_frame_ready_callback_t callback)
 {
        new_frame_ready_callback = callback;
-       has_new_frame_ready_callback = true;
 }
+
+void Mixer::OutputChannel::set_transition_names_updated_callback(Mixer::transition_names_updated_callback_t callback)
+{
+       transition_names_updated_callback = callback;
+}
+
+void Mixer::OutputChannel::set_name_updated_callback(Mixer::name_updated_callback_t callback)
+{
+       name_updated_callback = callback;
+}
+
+void Mixer::OutputChannel::set_color_updated_callback(Mixer::color_updated_callback_t callback)
+{
+       color_updated_callback = callback;
+}
+
+mutex RefCountedGLsync::fence_lock;