diff options
Diffstat (limited to 'content')
3 files changed, 172 insertions, 86 deletions
diff --git a/content/common/gpu/media/exynos_video_decode_accelerator.cc b/content/common/gpu/media/exynos_video_decode_accelerator.cc index b74d4d2..a046104 100644 --- a/content/common/gpu/media/exynos_video_decode_accelerator.cc +++ b/content/common/gpu/media/exynos_video_decode_accelerator.cc @@ -209,6 +209,7 @@ ExynosVideoDecodeAccelerator::ExynosVideoDecodeAccelerator( mfc_output_buffer_pixelformat_(0), mfc_output_dpb_size_(0), picture_clearing_count_(0), + pictures_assigned_(false, false), device_poll_thread_("ExynosDevicePollThread"), device_poll_interrupt_fd_(-1), make_context_current_(make_context_current), @@ -387,6 +388,8 @@ void ExynosVideoDecodeAccelerator::AssignPictureBuffers( return; } + // It's safe to manipulate all the buffer state here, because the decoder + // thread is waiting on pictures_assigned_. scoped_ptr<PictureBufferArrayRef> picture_buffers_ref( new PictureBufferArrayRef(egl_display_)); gfx::ScopedTextureBinder bind_restore(GL_TEXTURE_EXTERNAL_OES, 0); @@ -421,11 +424,29 @@ void ExynosVideoDecodeAccelerator::AssignPictureBuffers( picture_buffers_ref->picture_buffers.push_back( PictureBufferArrayRef::PictureBufferRef(egl_image, buffers[i].id())); } - decoder_thread_.message_loop()->PostTask( - FROM_HERE, - base::Bind(&ExynosVideoDecodeAccelerator::AssignPictureBuffersTask, - base::Unretained(this), - base::Passed(&picture_buffers_ref))); + + DCHECK(free_output_buffers_.empty()); + DCHECK_EQ(picture_buffers_ref->picture_buffers.size(), + mfc_output_buffer_map_.size()); + for (size_t i = 0; i < mfc_output_buffer_map_.size(); ++i) { + OutputRecord& output_record = mfc_output_buffer_map_[i]; + PictureBufferArrayRef::PictureBufferRef& buffer_ref = + picture_buffers_ref->picture_buffers[i]; + // We should be blank right now. + DCHECK(!output_record.at_device); + DCHECK(!output_record.at_client); + DCHECK_EQ(output_record.egl_image, EGL_NO_IMAGE_KHR); + DCHECK_EQ(output_record.egl_sync, EGL_NO_SYNC_KHR); + DCHECK_EQ(output_record.picture_id, -1); + DCHECK_EQ(output_record.cleared, false); + output_record.egl_image = buffer_ref.egl_image; + output_record.picture_id = buffer_ref.picture_id; + free_output_buffers_.push(i); + DVLOG(3) << "AssignPictureBuffers(): buffer[" << i + << "]: picture_id=" << buffer_ref.picture_id; + } + picture_buffers_ref->picture_buffers.clear(); + pictures_assigned_.Signal(); } void ExynosVideoDecodeAccelerator::ReusePictureBuffer(int32 picture_buffer_id) { @@ -479,6 +500,7 @@ void ExynosVideoDecodeAccelerator::Destroy() { if (decoder_thread_.IsRunning()) { decoder_thread_.message_loop()->PostTask(FROM_HERE, base::Bind( &ExynosVideoDecodeAccelerator::DestroyTask, base::Unretained(this))); + pictures_assigned_.Signal(); // DestroyTask() will cause the decoder_thread_ to flush all tasks. decoder_thread_.Stop(); } else { @@ -511,7 +533,7 @@ void ExynosVideoDecodeAccelerator::DecodeTask( NOTIFY_ERROR(UNREADABLE_INPUT); return; } - DVLOG(3) << "Decode(): mapped to addr=" << bitstream_record->shm->memory(); + DVLOG(3) << "DecodeTask(): mapped at=" << bitstream_record->shm->memory(); if (decoder_state_ == kResetting || decoder_flushing_) { // In the case that we're resetting or flushing, we need to delay decoding @@ -926,46 +948,6 @@ bool ExynosVideoDecodeAccelerator::FlushInputFrame() { return (decoder_state_ != kError); } -void ExynosVideoDecodeAccelerator::AssignPictureBuffersTask( - scoped_ptr<PictureBufferArrayRef> pic_buffers) { - DVLOG(3) << "AssignPictureBuffersTask()"; - DCHECK_EQ(decoder_thread_.message_loop(), base::MessageLoop::current()); - DCHECK_NE(decoder_state_, kUninitialized); - TRACE_EVENT0("Video Decoder", "EVDA::AssignPictureBuffersTask"); - - // We run AssignPictureBuffersTask even if we're in kResetting. - if (decoder_state_ == kError) { - DVLOG(2) << "AssignPictureBuffersTask(): early out: kError state"; - return; - } - - DCHECK_EQ(pic_buffers->picture_buffers.size(), mfc_output_buffer_map_.size()); - for (size_t i = 0; i < mfc_output_buffer_map_.size(); ++i) { - MfcOutputRecord& output_record = mfc_output_buffer_map_[i]; - PictureBufferArrayRef::PictureBufferRef& buffer_ref = - pic_buffers->picture_buffers[i]; - // We should be blank right now. - DCHECK(!output_record.at_device); - DCHECK(!output_record.at_client); - DCHECK_EQ(output_record.egl_image, EGL_NO_IMAGE_KHR); - DCHECK_EQ(output_record.egl_sync, EGL_NO_SYNC_KHR); - DCHECK_EQ(output_record.picture_id, -1); - DCHECK_EQ(output_record.cleared, false); - output_record.egl_image = buffer_ref.egl_image; - output_record.picture_id = buffer_ref.picture_id; - mfc_free_output_buffers_.push(i); - DVLOG(3) << "AssignPictureBuffersTask(): buffer[" << i - << "]: picture_id=" << buffer_ref.picture_id; - } - pic_buffers->picture_buffers.clear(); - - // We got buffers! Kick the MFC. - EnqueueMfc(); - - if (decoder_state_ == kChangingResolution) - ResumeAfterResolutionChange(); -} - void ExynosVideoDecodeAccelerator::ServiceDeviceTask(bool mfc_event_pending) { DVLOG(3) << "ServiceDeviceTask()"; DCHECK_EQ(decoder_thread_.message_loop(), base::MessageLoop::current()); @@ -1200,7 +1182,7 @@ bool ExynosVideoDecodeAccelerator::EnqueueMfcInputRecord() { input_record.at_device = true; mfc_input_buffer_queued_count_++; DVLOG(3) << "EnqueueMfcInputRecord(): enqueued input_id=" - << input_record.input_id; + << input_record.input_id << " size=" << input_record.bytes_used; return true; } @@ -1266,8 +1248,13 @@ void ExynosVideoDecodeAccelerator::ReusePictureBufferTask( break; if (index >= mfc_output_buffer_map_.size()) { - DLOG(ERROR) << "ReusePictureBufferTask(): picture_buffer_id not found"; - NOTIFY_ERROR(INVALID_ARGUMENT); + // It's possible that we've already posted a DismissPictureBuffer for this + // picture, but it has not yet executed when this ReusePictureBuffer was + // posted to us by the client. In that case just ignore this (we've already + // dismissed it and accounted for that) and let the sync object get + // destroyed. + DVLOG(4) << "ReusePictureBufferTask(): got picture id= " + << picture_buffer_id << " not in use (anymore?)."; return; } @@ -1279,6 +1266,7 @@ void ExynosVideoDecodeAccelerator::ReusePictureBufferTask( } DCHECK_EQ(output_record.egl_sync, EGL_NO_SYNC_KHR); + DCHECK(!output_record.at_device); output_record.at_client = false; output_record.egl_sync = egl_sync_ref->egl_sync; mfc_free_output_buffers_.push(index); @@ -1431,7 +1419,14 @@ void ExynosVideoDecodeAccelerator::ResetDoneTask() { // Jobs drained, we're finished resetting. DCHECK_EQ(decoder_state_, kResetting); - decoder_state_ = kAfterReset; + if (mfc_output_buffer_map_.empty()) { + // We must have gotten Reset() before we had a chance to request buffers + // from the client. + decoder_state_ = kInitialized; + } else { + decoder_state_ = kAfterReset; + } + decoder_partial_frame_pending_ = false; decoder_delay_bitstream_buffer_id_ = -1; child_message_loop_proxy_->PostTask(FROM_HERE, base::Bind( @@ -1521,15 +1516,22 @@ bool ExynosVideoDecodeAccelerator::StopDevicePoll(bool keep_mfc_input_state) { } mfc_input_buffer_queued_count_ = 0; } + while (!mfc_free_output_buffers_.empty()) mfc_free_output_buffers_.pop(); + for (size_t i = 0; i < mfc_output_buffer_map_.size(); ++i) { MfcOutputRecord& output_record = mfc_output_buffer_map_[i]; - // Only mark those free that aren't being held by the VDA client. + DCHECK(!(output_record.at_client && output_record.at_device)); + + // After streamoff, the device drops ownership of all buffers, even if + // we don't dequeue them explicitly. + mfc_output_buffer_map_[i].at_device = false; + // Some of them may still be owned by the client however. + // Reuse only those that aren't. if (!output_record.at_client) { DCHECK_EQ(output_record.egl_sync, EGL_NO_SYNC_KHR); mfc_free_output_buffers_.push(i); - mfc_output_buffer_map_[i].at_device = false; } } mfc_output_buffer_queued_count_ = 0; @@ -1595,6 +1597,7 @@ void ExynosVideoDecodeAccelerator::StartResolutionChangeIfNeeded() { void ExynosVideoDecodeAccelerator::FinishResolutionChange() { DCHECK_EQ(decoder_thread_.message_loop(), base::MessageLoop::current()); + DCHECK_EQ(decoder_state_, kChangingResolution); DVLOG(3) << "FinishResolutionChange()"; if (decoder_state_ == kError) { @@ -1617,8 +1620,7 @@ void ExynosVideoDecodeAccelerator::FinishResolutionChange() { return; } - // From here we stay in kChangingResolution and wait for - // AssignPictureBuffers() before we can resume. + ResumeAfterResolutionChange(); } void ExynosVideoDecodeAccelerator::ResumeAfterResolutionChange() { @@ -1726,7 +1728,7 @@ bool ExynosVideoDecodeAccelerator::GetFormatInfo(struct v4l2_format* format, *again = true; return true; } else { - DPLOG(ERROR) << "DecodeBufferInitial(): ioctl() failed: VIDIOC_G_FMT"; + DPLOG(ERROR) << __func__ << "(): ioctl() failed: VIDIOC_G_FMT"; NOTIFY_ERROR(PLATFORM_FAILURE); return false; } @@ -1865,6 +1867,22 @@ bool ExynosVideoDecodeAccelerator::CreateMfcOutputBuffers() { frame_buffer_size_, GL_TEXTURE_EXTERNAL_OES)); + // Wait for the client to call AssignPictureBuffers() on the Child thread. + // We do this, because if we continue decoding without finishing buffer + // allocation, we may end up Resetting before AssignPictureBuffers arrives, + // resulting in unnecessary complications and subtle bugs. + // For example, if the client calls Decode(Input1), Reset(), Decode(Input2) + // in a sequence, and Decode(Input1) results in us getting here and exiting + // without waiting, we might end up running Reset{,Done}Task() before + // AssignPictureBuffers is scheduled, thus cleaning up and pushing buffers + // to the free_output_buffers_ map twice. If we somehow marked buffers as + // not ready, we'd need special handling for restarting the second Decode + // task and delaying it anyway. + // Waiting here is not very costly and makes reasoning about different + // situations much simpler. + pictures_assigned_.Wait(); + + Enqueue(); return true; } diff --git a/content/common/gpu/media/exynos_video_decode_accelerator.h b/content/common/gpu/media/exynos_video_decode_accelerator.h index 7ccd594..71b5d66 100644 --- a/content/common/gpu/media/exynos_video_decode_accelerator.h +++ b/content/common/gpu/media/exynos_video_decode_accelerator.h @@ -14,6 +14,7 @@ #include "base/callback_forward.h" #include "base/memory/linked_ptr.h" #include "base/memory/scoped_ptr.h" +#include "base/synchronization/waitable_event.h" #include "base/threading/thread.h" #include "content/common/content_export.h" #include "content/common/gpu/media/video_decode_accelerator_impl.h" @@ -46,16 +47,27 @@ class H264Parser; // media::VideoDecodeAccelerator::Client interface. // * The decoder_thread_, owned by this class. It services API tasks, through // the *Task() routines, as well as V4L2 device events, through -// ServiceDeviceTask(). Almost all state modification is done on this thread. +// ServiceDeviceTask(). Almost all state modification is done on this thread +// (this doesn't include buffer (re)allocation sequence, see below). // * The device_poll_thread_, owned by this class. All it does is epoll() on // the V4L2 in DevicePollTask() and schedule a ServiceDeviceTask() on the // decoder_thread_ when something interesting happens. // TODO(sheu): replace this thread with an TYPE_IO decoder_thread_. // -// Note that this class has no locks! Everything's serviced on the -// decoder_thread_, so there are no synchronization issues. +// Note that this class has (almost) no locks, apart from the pictures_assigned_ +// WaitableEvent. Everything (apart from buffer (re)allocation) is serviced on +// the decoder_thread_, so there are no synchronization issues. // ... well, there are, but it's a matter of getting messages posted in the // right order, not fiddling with locks. +// Buffer creation is a two-step process that is serviced partially on the +// Child thread, because we need to wait for the client to provide textures +// for the buffers we allocate. We cannot keep the decoder thread running while +// the client allocates Pictures for us, because we need to REQBUFS first to get +// the required number of output buffers from the device and that cannot be done +// unless we free the previous set of buffers, leaving the decoding in a +// inoperable state for the duration of the wait for Pictures. So to prevent +// subtle races (esp. if we get Reset() in the meantime), we block the decoder +// thread while we wait for AssignPictureBuffers from the client. class CONTENT_EXPORT ExynosVideoDecodeAccelerator : public VideoDecodeAcceleratorImpl { public: @@ -122,7 +134,7 @@ class CONTENT_EXPORT ExynosVideoDecodeAccelerator struct BitstreamBufferRef; // Auto-destruction reference for an array of PictureBuffer, for - // message-passing from AssignPictureBuffers() to AssignPictureBuffersTask(). + // simpler EGLImage cleanup if any calls fail in AssignPictureBuffers(). struct PictureBufferArrayRef; // Auto-destruction reference for EGLSync (for message-passing). @@ -185,11 +197,6 @@ class CONTENT_EXPORT ExynosVideoDecodeAccelerator // Flush data for one decoded frame. bool FlushInputFrame(); - // Process an AssignPictureBuffers() API call. After this, the - // device_poll_thread_ can be started safely, since we have all our - // buffers. - void AssignPictureBuffersTask(scoped_ptr<PictureBufferArrayRef> pic_buffers); - // Service I/O on the V4L2 devices. This task should only be scheduled from // DevicePollTask(). If |mfc_event_pending| is true, one or more events // on MFC file descriptor are pending. @@ -357,6 +364,9 @@ class CONTENT_EXPORT ExynosVideoDecodeAccelerator // // Hardware state and associated queues. Since decoder_thread_ services // the hardware, decoder_thread_ owns these too. + // mfc_output_buffer_map_ and free_output_buffers_ are an exception during the + // buffer (re)allocation sequence, when the decoder_thread_ is blocked briefly + // while the Child thread manipulates them. // // Completed decode buffers, waiting for MFC. @@ -394,6 +404,10 @@ class CONTENT_EXPORT ExynosVideoDecodeAccelerator // The number of pictures that are sent to PictureReady and will be cleared. int picture_clearing_count_; + // Used by the decoder thread to wait for AssignPictureBuffers to arrive + // to avoid races with potential Reset requests. + base::WaitableEvent pictures_assigned_; + // Output picture size. gfx::Size frame_buffer_size_; diff --git a/content/common/gpu/media/video_decode_accelerator_unittest.cc b/content/common/gpu/media/video_decode_accelerator_unittest.cc index 7c1f077..bc1c282 100644 --- a/content/common/gpu/media/video_decode_accelerator_unittest.cc +++ b/content/common/gpu/media/video_decode_accelerator_unittest.cc @@ -49,6 +49,7 @@ #include "content/common/gpu/media/rendering_helper.h" #include "content/common/gpu/media/video_accelerator_unittest_helpers.h" #include "content/public/common/content_switches.h" +#include "media/filters/h264_parser.h" #include "ui/gfx/codec/png_codec.h" #if defined(OS_WIN) @@ -104,6 +105,8 @@ bool g_disable_rendering = false; // Magic constants for differentiating the reasons for NotifyResetDone being // called. enum ResetPoint { + // Reset() just after calling Decode() with a fragment containing config info. + RESET_AFTER_FIRST_CONFIG_INFO = -4, START_OF_STREAM_RESET = -3, MID_STREAM_RESET = -2, END_OF_STREAM_RESET = -1 @@ -124,7 +127,7 @@ struct TestVideoFile { num_fragments(-1), min_fps_render(-1), min_fps_no_render(-1), - profile(-1), + profile(media::VIDEO_CODEC_PROFILE_UNKNOWN), reset_after_frame_num(END_OF_STREAM_RESET) { } @@ -135,7 +138,7 @@ struct TestVideoFile { int num_fragments; int min_fps_render; int min_fps_no_render; - int profile; + media::VideoCodecProfile profile; int reset_after_frame_num; std::string data_str; }; @@ -376,7 +379,7 @@ class GLRenderingVDAClient int delete_decoder_state, int frame_width, int frame_height, - int profile, + media::VideoCodecProfile profile, double rendering_fps, bool suppress_rendering, int delay_reuse_after_frame_num, @@ -455,7 +458,7 @@ class GLRenderingVDAClient int num_done_bitstream_buffers_; PictureBufferById picture_buffers_by_id_; base::TimeTicks initialize_done_ticks_; - int profile_; + media::VideoCodecProfile profile_; GLenum texture_target_; bool suppress_rendering_; std::vector<base::TimeTicks> frame_delivery_times_; @@ -482,7 +485,7 @@ GLRenderingVDAClient::GLRenderingVDAClient( int delete_decoder_state, int frame_width, int frame_height, - int profile, + media::VideoCodecProfile profile, double rendering_fps, bool suppress_rendering, int delay_reuse_after_frame_num, @@ -567,10 +570,9 @@ void GLRenderingVDAClient::CreateAndStartDecoder() { return; // Configure the decoder. - media::VideoCodecProfile profile = media::H264PROFILE_BASELINE; - if (profile_ != -1) - profile = static_cast<media::VideoCodecProfile>(profile_); - CHECK(decoder_->Initialize(profile)); + profile_ = (profile_ != media::VIDEO_CODEC_PROFILE_UNKNOWN ? + profile_ : media::H264PROFILE_BASELINE); + CHECK(decoder_->Initialize(profile_)); } void GLRenderingVDAClient::ProvidePictureBuffers( @@ -663,6 +665,7 @@ void GLRenderingVDAClient::NotifyInitializeDone() { initialize_done_ticks_ = base::TimeTicks::Now(); if (reset_after_frame_num_ == START_OF_STREAM_RESET) { + reset_after_frame_num_ = MID_STREAM_RESET; decoder_->Reset(); return; } @@ -834,6 +837,29 @@ std::string GLRenderingVDAClient::GetBytesForNextFrame( return bytes; } +static bool FragmentHasConfigInfo(const uint8* data, size_t size, + media::VideoCodecProfile profile) { + if (profile >= media::H264PROFILE_MIN && + profile <= media::H264PROFILE_MAX) { + media::H264Parser parser; + parser.SetStream(data, size); + media::H264NALU nalu; + media::H264Parser::Result result = parser.AdvanceToNextNALU(&nalu); + if (result != media::H264Parser::kOk) { + // Let the VDA figure out there's something wrong with the stream. + return false; + } + + return nalu.nal_unit_type == media::H264NALU::kSPS; + } else if (profile >= media::VP8PROFILE_MIN && + profile <= media::VP8PROFILE_MAX) { + return (size > 0 && !(data[0] & 0x01)); + } + + CHECK(false) << "Invalid profile"; // Shouldn't happen at this point. + return false; +} + void GLRenderingVDAClient::DecodeNextFragment() { if (decoder_deleted()) return; @@ -854,6 +880,19 @@ void GLRenderingVDAClient::DecodeNextFragment() { } size_t next_fragment_size = next_fragment_bytes.size(); + // Call Reset() just after Decode() if the fragment contains config info. + // This tests how the VDA behaves when it gets a reset request before it has + // a chance to ProvidePictureBuffers(). + bool reset_here = false; + if (reset_after_frame_num_ == RESET_AFTER_FIRST_CONFIG_INFO) { + reset_here = FragmentHasConfigInfo( + reinterpret_cast<const uint8*>(next_fragment_bytes.data()), + next_fragment_size, + profile_); + if (reset_here) + reset_after_frame_num_ = END_OF_STREAM_RESET; + } + // Populate the shared memory buffer w/ the fragment, duplicate its handle, // and hand it off to the decoder. base::SharedMemory shm; @@ -868,13 +907,20 @@ void GLRenderingVDAClient::DecodeNextFragment() { next_bitstream_buffer_id_ = (next_bitstream_buffer_id_ + 1) & 0x3FFFFFFF; decoder_->Decode(bitstream_buffer); ++outstanding_decodes_; - encoded_data_next_pos_to_decode_ = end_pos; - if (!remaining_play_throughs_ && -delete_decoder_state_ == next_bitstream_buffer_id_) { DeleteDecoder(); } + if (reset_here) { + reset_after_frame_num_ = MID_STREAM_RESET; + decoder_->Reset(); + // Restart from the beginning to re-Decode() the SPS we just sent. + encoded_data_next_pos_to_decode_ = 0; + } else { + encoded_data_next_pos_to_decode_ = end_pos; + } + if (decode_calls_per_second_ > 0) { base::MessageLoop::current()->PostDelayedTask( FROM_HERE, @@ -1006,8 +1052,10 @@ void VideoDecodeAcceleratorTest::ParseAndReadTestVideoData( CHECK(base::StringToInt(fields[5], &video_file->min_fps_render)); if (!fields[6].empty()) CHECK(base::StringToInt(fields[6], &video_file->min_fps_no_render)); + int profile = -1; if (!fields[7].empty()) - CHECK(base::StringToInt(fields[7], &video_file->profile)); + CHECK(base::StringToInt(fields[7], &profile)); + video_file->profile = static_cast<media::VideoCodecProfile>(profile); // Read in the video data. base::FilePath filepath(video_file->file_name); @@ -1024,14 +1072,18 @@ void VideoDecodeAcceleratorTest::UpdateTestVideoFileParams( std::vector<TestVideoFile*>* test_video_files) { for (size_t i = 0; i < test_video_files->size(); i++) { TestVideoFile* video_file = (*test_video_files)[i]; - if (video_file->num_frames > 0 && reset_point == MID_STREAM_RESET) { - // Reset should not go beyond the last frame; reset after the first - // frame for short videos. + if (reset_point == MID_STREAM_RESET) { + // Reset should not go beyond the last frame; + // reset in the middle of the stream for short videos. video_file->reset_after_frame_num = kMaxResetAfterFrameNum; - if (video_file->num_frames <= kMaxResetAfterFrameNum) - video_file->reset_after_frame_num = 1; + if (video_file->num_frames <= video_file->reset_after_frame_num) + video_file->reset_after_frame_num = video_file->num_frames / 2; + video_file->num_frames += video_file->reset_after_frame_num; + } else { + video_file->reset_after_frame_num = reset_point; } + if (video_file->min_fps_render != -1) video_file->min_fps_render /= num_concurrent_decoders; if (video_file->min_fps_no_render != -1) @@ -1353,16 +1405,18 @@ INSTANTIATE_TEST_CASE_P( ::testing::Values( MakeTuple(1, 1, 4, END_OF_STREAM_RESET, CS_RESET, false, false))); -// This hangs on Exynos, preventing further testing and wasting test machine -// time. -// TODO(ihf): Enable again once http://crbug.com/269754 is fixed. -#if defined(ARCH_CPU_X86_FAMILY) // Test that Reset() before the first Decode() works fine. INSTANTIATE_TEST_CASE_P( ResetBeforeDecode, VideoDecodeAcceleratorParamTest, ::testing::Values( MakeTuple(1, 1, 1, START_OF_STREAM_RESET, CS_RESET, false, false))); -#endif // ARCH_CPU_X86_FAMILY + +// Test Reset() immediately after Decode() containing config info. +INSTANTIATE_TEST_CASE_P( + ResetAfterFirstConfigInfo, VideoDecodeAcceleratorParamTest, + ::testing::Values( + MakeTuple( + 1, 1, 1, RESET_AFTER_FIRST_CONFIG_INFO, CS_RESET, false, false))); // Test that Reset() mid-stream works fine and doesn't affect decoding even when // Decode() calls are made during the reset. |