Monopoly — Portage moderne 0.1.0
Portage moderne C/C++23 du Monopoly de 1999
Loading...
Searching...
No Matches
SequenceVideoRuntime.cpp
Go to the documentation of this file.
2
3#include "Timers.hpp"
4
5#include <algorithm>
6#include <cmath>
7#include <fstream>
8#include <limits>
9#include <set>
10#include <utility>
11
12namespace monopoly::video
13{
14 namespace
15 {
16 constexpr std::uintmax_t MaximumVideoBytes =
17 1024ULL * 1024ULL * 1024ULL;
18
19 [[nodiscard]] std::expected<std::filesystem::path, std::string>
21 const data::ResourcePaths& paths,
22 std::string_view legacyName)
23 {
24 if (legacyName.empty())
25 return std::unexpected(
26 "video sequence has no external filename");
27
28 auto direct = paths.resolve(legacyName);
29 if (direct) return *direct;
30 std::string aviName{"AVI/"};
31 aviName.append(legacyName);
32 auto avi = paths.resolve(aviName);
33 if (avi) return *avi;
34
35 return std::unexpected(
36 "video file cannot be resolved from resource roots: " +
37 std::string(legacyName));
38 }
39
40 [[nodiscard]] std::expected<std::vector<std::uint8_t>, std::string>
41 readVideoBytes(const std::filesystem::path& path)
42 {
43 std::error_code error;
44 const auto size = std::filesystem::file_size(path, error);
45 if (error)
46 return std::unexpected(
47 "cannot inspect video file: " + error.message());
48 if (size == 0 || size > MaximumVideoBytes ||
49 size > std::numeric_limits<std::size_t>::max())
50 return std::unexpected(
51 "video file size is outside runtime limits");
52
53 std::ifstream stream(path, std::ios::binary);
54 if (!stream)
55 return std::unexpected("cannot open video file");
56 std::vector<std::uint8_t> bytes(
57 static_cast<std::size_t>(size));
58 stream.read(
59 reinterpret_cast<char*>(bytes.data()),
60 static_cast<std::streamsize>(bytes.size()));
61 if (!stream ||
62 static_cast<std::size_t>(stream.gcount()) != bytes.size())
63 return std::unexpected("cannot read complete video file");
64 return bytes;
65 }
66
67 [[nodiscard]] std::expected<std::uint64_t, std::string>
68 sequenceMicroseconds(std::int32_t clock)
69 {
70 if (clock <= 0) return 0;
71 constexpr auto million = std::uint64_t{1'000'000};
72 const auto ticks = static_cast<std::uint64_t>(clock);
73 if (ticks >
74 std::numeric_limits<std::uint64_t>::max() / million)
75 return std::unexpected(
76 "video sequence clock exceeds timeline range");
78 }
79
80 [[nodiscard]] std::expected<
81 data::Sequence2DBoundingBoxAttribute, std::string>
83 data::SequenceVideoData& options,
84 const AviMetadata& metadata,
85 const std::optional<data::Sequence2DBoundingBoxAttribute>& requested)
86 {
87 if (options.enableVideo &&
88 options.doubleAlternateLines &&
89 !options.drawDirectlyToScreen)
90 options.doubleAlternateLines = false;
91
92 if (requested)
93 {
94 auto result = *requested;
95 if (options.enableVideo && options.doubleAlternateLines)
96 {
97 const auto width =
98 static_cast<std::int64_t>(result.right) - result.left;
99 const auto height =
100 static_cast<std::int64_t>(result.bottom) - result.top;
101 if (width != static_cast<std::int64_t>(metadata.width) * 2 ||
102 height != static_cast<std::int64_t>(metadata.height) * 2)
103 options.doubleAlternateLines = false;
104 }
105 return result;
106 }
107
108 data::Sequence2DBoundingBoxAttribute result{};
109 if (!options.enableVideo)
110 {
111 result.right = 1;
112 result.bottom = 1;
113 return result;
114 }
115
116 const std::uint64_t scale =
117 options.doubleAlternateLines ? 2U : 1U;
118 const auto width =
119 static_cast<std::uint64_t>(metadata.width) * scale;
120 const auto height =
121 static_cast<std::uint64_t>(metadata.height) * scale;
122 if (width >
123 static_cast<std::uint64_t>(
124 std::numeric_limits<std::int32_t>::max()) ||
125 height >
126 static_cast<std::uint64_t>(
127 std::numeric_limits<std::int32_t>::max()))
128 return std::unexpected(
129 "video bounding box exceeds 2D runtime range");
130
131 result.right = static_cast<std::int32_t>(width);
132 result.bottom = static_cast<std::int32_t>(height);
133 return result;
134 }
135 }
136 std::expected<SequenceRuntimeBridge::Active*, std::string>
137 SequenceRuntimeBridge::active(sequence::SequenceNodeId node)
138 {
139 const auto found = active_.find(node);
140 if (found == active_.end())
141 return std::unexpected(
142 "video command references an inactive sequence");
143 return &found->second;
144 }
145
146 std::expected<std::vector<SequenceJumpNotice>, std::string>
149 const data::ResourceSnapshot& resources)
150 {
151 const auto intents = sequences.videoInstances();
152 std::set<sequence::SequenceNodeId> live;
153 std::vector<SequenceJumpNotice> jumps;
154 states_.clear();
155 states_.reserve(intents.size());
156
157 for (const auto& intent : intents)
158 {
159 live.insert(intent.node);
160 auto found = active_.find(intent.node);
161 if (found == active_.end())
162 {
163 auto file = resolveVideoPath(
164 resources.paths(), intent.fileName);
165 if (!file) return std::unexpected(file.error());
166
167 auto bytes = readVideoBytes(*file);
168 if (!bytes) return std::unexpected(bytes.error());
169
170 Active created{};
171 created.file = *file;
172 created.options = intent.video;
173 const auto opened = created.runtime.open(*bytes, false);
174 if (!opened) return std::unexpected(opened.error());
175 const auto bounds = effectiveBoundingBox(created.options,
176 created.runtime.metadata(), intent.boundingBox);
177 if (!bounds) return std::unexpected(bounds.error());
178 created.boundingBox = *bounds;
179
180 found = active_.emplace(
181 intent.node, std::move(created)).first;
182 }
183
184 auto& entry = found->second;
185 if (entry.file.empty())
186 return std::unexpected(
187 "active video sequence lost its resolved file");
188
189 if (!intent.video.enableAudio)
190 {
191 const auto elapsed = sequenceMicroseconds(intent.elapsedParentClock);
192 if (!elapsed) return std::unexpected(elapsed.error());
193 const auto desired =
194 entry.runtime.frameAtElapsed(*elapsed);
195 if (!desired) return std::unexpected(desired.error());
196 if (entry.lastSequenceClock !=
197 std::numeric_limits<std::int32_t>::min() &&
198 intent.elapsedParentClock < entry.lastSequenceClock &&
199 *desired < entry.runtime.status().numberOfFrames)
200 {
201 const auto cut = entry.runtime.cutToFrame(*desired);
202 if (!cut) return std::unexpected(cut.error());
203 }
204 else
205 {
206 const auto fed = entry.runtime.feedToFrame(*desired);
207 if (!fed) return std::unexpected(fed.error());
208 }
209 }
210
211 entry.lastSequenceClock = intent.elapsedParentClock;
212 if (auto jump = entry.runtime.takeJumpEvent())
213 jumps.push_back({intent.node, *jump});
214
215 states_.push_back({
216 intent.node,
217 entry.file,
218 entry.runtime.status(),
219 entry.options,
220 entry.boundingBox,
221 intent.worldTransform,
222 intent.video.enableAudio, true});
223 }
224 for (auto it = active_.begin(); it != active_.end();)
225 {
226 if (!live.contains(it->first))
227 it = active_.erase(it);
228 else
229 ++it;
230 }
231
232 return jumps;
233 }
234
235 std::expected<std::vector<SequenceJumpNotice>, std::string>
237 {
238 const auto resources = playback.resources();
239 if (!resources) return std::unexpected("video presentation has no resource snapshot");
240 const auto intents = playback.runtime().videoInstances();
241 std::set<sequence::SequenceNodeId> live;
242 for (const auto& intent : intents) live.insert(intent.node);
243 // Stop/remove resources before decoder destruction; no worker sees DATA/SDL GPU state.
244 for (auto it = active_.begin(); it != active_.end();)
245 {
246 if (live.contains(it->first)) { ++it; continue; }
247 auto& entry = it->second;
248 if (entry.surfaceVisible && entry.surface)
249 {
250 const auto stopped = playback.stop(*entry.surface, entry.publishedPriority);
251 if (!stopped) return std::unexpected(stopped.error());
252 }
253 if (entry.surface) (void)playback.runtimeBitmaps().remove(*entry.surface);
254 it = active_.erase(it);
255 }
256 states_.clear();
257 states_.reserve(intents.size());
258 std::vector<SequenceJumpNotice> jumps;
259 for (const auto& intent : intents)
260 {
261 auto found = active_.find(intent.node);
262 if (found == active_.end())
263 {
264 const auto file = resolveVideoPath(resources->paths(), intent.fileName);
265 if (!file) return std::unexpected(file.error());
266 Active created;
267 created.file = *file;
268 created.options = intent.video;
269 created.loop = intent.endingAction == 3;
270 created.presentation = std::make_shared<Presentation>();
271 auto decoderOptions = decoderOptions_;
272 decoderOptions.decodeVideo = intent.video.enableVideo;
273 const auto opened = created.presentation->open(*file,
274 intent.video.enableAudio, decoderOptions);
275 if (!opened) return std::unexpected(opened.error());
276 found = active_.emplace(intent.node, std::move(created)).first;
277 }
278 auto& entry = found->second;
279 entry.loop = intent.endingAction == 3;
280 if (!entry.presentation)
281 return std::unexpected("cannot mix metadata-only and decoded video bridge sessions");
282 auto snapshot = entry.presentation->snapshot();
283 if (snapshot.phase == DecoderPhase::Failed)
284 return std::unexpected(snapshot.error);
285 if (!snapshot.metadata)
286 {
287 states_.push_back({intent.node, entry.file, {}, entry.options,
288 intent.boundingBox, intent.worldTransform, intent.video.enableAudio});
289 continue;
290 }
291 const auto& metadata = *snapshot.metadata;
292 if (!entry.runtime.open())
293 {
294 if (!metadata.frameRateNumerator || !metadata.frameRateDenominator ||
295 !metadata.durationMicroseconds)
296 return std::unexpected("decoded video has invalid duration or frame cadence");
297 const auto duration = (1000000ULL * metadata.frameRateDenominator +
298 metadata.frameRateNumerator / 2U) / metadata.frameRateNumerator;
299 const long double frames = std::ceil(
300 static_cast<long double>(metadata.durationMicroseconds) *
301 metadata.frameRateNumerator / (1000000.0L * metadata.frameRateDenominator));
302 if (!duration || duration > std::numeric_limits<std::uint32_t>::max() ||
303 frames < 1 || frames > std::numeric_limits<std::int32_t>::max())
304 return std::unexpected("decoded video timeline exceeds runtime range");
305 const auto opened = entry.runtime.open(AviMetadata{
306 static_cast<std::uint32_t>(duration), static_cast<std::uint32_t>(frames),
307 metadata.width, metadata.height});
308 if (!opened) return std::unexpected(opened.error());
309 const auto bounds = effectiveBoundingBox(entry.options,
310 entry.runtime.metadata(), intent.boundingBox);
311 if (!bounds) return std::unexpected(bounds.error());
312 entry.boundingBox = intent.binkDoubleSize ?
313 binkDoubleSizeBounds(metadata.width, metadata.height) : *bounds;
314 const auto durationTicks = std::ceil(static_cast<long double>(
315 metadata.durationMicroseconds) * timers::BasicClockRateHz / 1000000.0L);
316 if (durationTicks < 1 || durationTicks > std::numeric_limits<std::int32_t>::max())
317 return std::unexpected("decoded video duration exceeds sequence clock range");
318 entry.durationTicks = static_cast<std::int32_t>(durationTicks);
319 entry.clipDurationMicroseconds = metadata.durationMicroseconds;
320 if (const auto node = playback.runtime().inspect(intent.node))
321 {
322 const auto authoredDuration = sequenceMicroseconds(node->endTime);
323 if (!authoredDuration) return std::unexpected(authoredDuration.error());
324 entry.authoredClip = *authoredDuration < metadata.durationMicroseconds;
325 entry.clipDurationMicroseconds = std::min(
326 entry.clipDurationMicroseconds, *authoredDuration);
327 }
328 }
329 const auto sequenceTime = sequenceMicroseconds(intent.elapsedParentClock);
330 if (!sequenceTime) return std::unexpected(sequenceTime.error());
331 if (entry.lastSequenceClock != std::numeric_limits<std::int32_t>::min() &&
332 intent.elapsedParentClock < entry.lastSequenceClock && !entry.seekPending)
333 {
334 const auto frame = entry.runtime.frameAtElapsed(*sequenceTime);
335 if (!frame) return std::unexpected(frame.error());
336 const auto cut = entry.runtime.cutToFrame(
337 std::min(*frame, entry.runtime.status().numberOfFrames - 1));
338 if (!cut) return std::unexpected(cut.error());
339 entry.requestedSeek = static_cast<std::uint64_t>(entry.runtime.status().currentFrame) *
340 entry.runtime.metadata().microsecondsPerFrame;
341 entry.seekPending = true;
342 }
343 if (entry.seekPending)
344 {
345 const auto seeked = entry.presentation->seek(entry.requestedSeek, *sequenceTime);
346 if (!seeked) return std::unexpected(seeked.error());
347 entry.seekPending = false;
348 }
349 bool paused{};
350 for (auto node = playback.runtime().inspect(intent.node); node;
351 node = node->parent ? playback.runtime().inspect(node->parent) : std::nullopt)
352 paused = paused || node->paused;
353 const auto gain = entry.presentation->setGain(
354 static_cast<float>(intent.volume) / 100.0F);
355 if (!gain) return std::unexpected(gain.error());
356 if (intent.pitch != 0U && metadata.audioSampleRate == 0U)
357 return std::unexpected(
358 "video audio source sample rate is unavailable for legacy pitch");
359 const auto pitch = entry.presentation->setPitch(
360 intent.pitch, metadata.audioSampleRate);
361 if (!pitch) return std::unexpected(pitch.error());
362 entry.presentation->setPanning(intent.panning);
363 const auto clock = entry.presentation->pump(
364 *sequenceTime, paused || entry.runtime.status().ended);
365 if (!clock) return std::unexpected(clock.error());
366 const auto desired = entry.runtime.frameAtElapsed(std::min(clock->elapsedMicroseconds,
367 entry.clipDurationMicroseconds > 0 ? entry.clipDurationMicroseconds - 1 : 0));
368 if (!desired) return std::unexpected(desired.error());
369 if (!entry.runtime.status().ended)
370 {
371 const auto fed = entry.runtime.feedToFrame(
372 std::min(*desired, entry.runtime.status().numberOfFrames - 1));
373 if (!fed) return std::unexpected(fed.error());
374 }
375 bool jumped{};
376 if (auto event = entry.runtime.takeJumpEvent())
377 {
378 jumps.push_back({intent.node, *event});
379 if (event->alternativeTaken)
380 {
381 const auto timestamp = static_cast<std::uint64_t>(event->jumpToFrame) *
382 entry.runtime.metadata().microsecondsPerFrame;
383 const auto seeked = entry.presentation->seek(timestamp, *sequenceTime);
384 if (!seeked) return std::unexpected(seeked.error());
385 jumped = true;
386 }
387 }
388 const auto frameTime = jumped ?
389 static_cast<std::uint64_t>(entry.runtime.status().currentFrame) *
390 entry.runtime.metadata().microsecondsPerFrame : std::min(clock->elapsedMicroseconds,
391 entry.clipDurationMicroseconds > 0 ? entry.clipDurationMicroseconds - 1 : 0);
392 const auto decisionLimit = static_cast<std::uint64_t>(
393 std::max(entry.runtime.status().currentFrame, 0)) + 1U;
394 const auto frameEndTime = decisionLimit *
395 entry.runtime.metadata().microsecondsPerFrame - 1;
396 auto frame = entry.presentation->frameAt(std::min(frameTime, frameEndTime));
397 if (!frame) return std::unexpected(frame.error());
398 if (entry.options.enableVideo)
399 {
400 const auto transform = videoFrameTransform(metadata.width, metadata.height,
401 entry.boundingBox, intent.worldTransform);
402 if (!transform) return std::unexpected(transform.error());
403 const bool moved = entry.surfaceVisible &&
404 (entry.publishedTransform.values != transform->values ||
405 entry.publishedPriority != intent.priority);
406 const std::size_t commands = !entry.surfaceVisible && *frame ? 1U :
407 moved ? (entry.publishedPriority != intent.priority ? 2U : 1U) : 0U;
408 if (commands > sequence::SequenceCommandQueue::Capacity - playback.commands().pendingCount())
409 return std::unexpected("sequence command queue cannot fit decoded video presentation");
410 if (*frame)
411 {
412 auto image = prepareVideoFrame(std::move(**frame), entry.options);
413 if (!image) return std::unexpected(image.error());
414 if (!entry.surface)
415 {
416 const auto created = playback.runtimeBitmaps().create(
417 image->width, image->height, !entry.options.drawSolid);
418 if (!created) return std::unexpected(created.error());
419 entry.surface = *created;
420 }
421 const auto updated = playback.runtimeBitmaps().update(*entry.surface, std::move(*image));
422 if (!updated) return std::unexpected(updated.error());
423 }
424 if (entry.surface && (!entry.surfaceVisible || moved))
425 {
426 if (entry.surfaceVisible && entry.publishedPriority != intent.priority)
427 {
428 const auto stopped = playback.stop(*entry.surface, entry.publishedPriority);
429 if (!stopped) return std::unexpected(stopped.error());
430 entry.surfaceVisible = false;
431 }
432 const auto published = entry.surfaceVisible ?
433 playback.move(*entry.surface, intent.priority, *transform) :
434 playback.startMoved(*entry.surface, intent.priority, *transform);
435 if (!published) return std::unexpected(published.error());
436 entry.surfaceVisible = true;
437 entry.publishedTransform = *transform;
438 entry.publishedPriority = intent.priority;
439 }
440 }
441 snapshot = entry.presentation->snapshot();
442 const bool decodedEnd = snapshot.phase == DecoderPhase::Ended &&
443 entry.presentation->videoDrained() && clock->audioDrained &&
444 clock->elapsedMicroseconds >= metadata.durationMicroseconds;
445 const bool ended = !jumped && (decodedEnd ||
446 (entry.authoredClip && clock->elapsedMicroseconds >= entry.clipDurationMicroseconds));
447 const bool finishSequence = ended || entry.runtime.status().ended;
448 if (ended && (!entry.runtime.status().ended || entry.loop))
449 {
450 if (entry.loop)
451 {
452 const auto seeked = entry.presentation->seek(0, *sequenceTime);
453 if (!seeked) return std::unexpected(seeked.error());
454 const auto cut = entry.runtime.cutToFrame(0);
455 if (!cut) return std::unexpected(cut.error());
456 }
457 else
458 {
459 const auto fed = entry.runtime.feedToFrame(entry.runtime.status().numberOfFrames);
460 if (!fed) return std::unexpected(fed.error());
461 }
462 }
463 if (finishSequence && !entry.loop)
464 {
465 const auto pausedAtEnd = entry.presentation->pump(*sequenceTime, true);
466 if (!pausedAtEnd) return std::unexpected(pausedAtEnd.error());
467 if (intent.endingAction <= 1 && entry.surface)
468 {
469 if (entry.surfaceVisible)
470 {
471 const auto stopped = playback.stop(*entry.surface, entry.publishedPriority);
472 if (!stopped) return std::unexpected(stopped.error());
473 }
474 (void)playback.runtimeBitmaps().remove(*entry.surface);
475 entry.surface.reset();
476 entry.surfaceVisible = false;
477 }
478 }
479 const auto mediaTicks = static_cast<std::int32_t>(std::min<long double>(
480 static_cast<long double>(clock->elapsedMicroseconds) * timers::BasicClockRateHz / 1000000.0L,
481 entry.durationTicks));
482 const auto supplied = playback.runtime().requestVideoClock(
483 intent.node, mediaTicks, entry.durationTicks, finishSequence);
484 if (!supplied) return std::unexpected(supplied.error().detail);
485 entry.lastSequenceClock = intent.elapsedParentClock;
486 states_.push_back({intent.node, entry.file, entry.runtime.status(), entry.options,
487 entry.boundingBox, intent.worldTransform, clock->waitingForAudio,
488 true, entry.surface, clock->consumedAudioBytes});
489 }
490 return jumps;
491 }
492
493 std::expected<void, std::string> SequenceRuntimeBridge::reset(
495 {
496 const auto stops = static_cast<std::size_t>(std::count_if(active_.begin(), active_.end(),
497 [](const auto& entry) { return entry.second.surfaceVisible; }));
498 if (stops > sequence::SequenceCommandQueue::Capacity - playback.commands().pendingCount())
499 return std::unexpected("sequence command queue cannot fit video reset");
500 for (const auto& [node, entry] : active_)
501 {
502 (void)node;
503 if (entry.surface)
504 {
505 if (entry.surfaceVisible)
506 {
507 // Ordered after an unexecuted Start as well as after live roots.
508 const auto stopped = playback.stop(*entry.surface, entry.publishedPriority);
509 if (!stopped) return stopped;
510 }
511 (void)playback.runtime().stopMatching(*entry.surface, entry.publishedPriority);
512 (void)playback.runtimeBitmaps().remove(*entry.surface);
513 }
514 }
515 reset();
516 return {};
517 }
518 std::expected<void, std::string> SequenceRuntimeBridge::setAlternative(
520 std::int32_t decisionFrame,
521 std::int32_t jumpToFrame)
522 {
523 auto entry = active(node);
524 if (!entry) return std::unexpected(entry.error());
525 return (*entry)->runtime.setAlternative(
526 decisionFrame, jumpToFrame);
527 }
528
529 std::expected<void, std::string> SequenceRuntimeBridge::chooseAlternative(
531 std::int32_t decisionFrame,
532 bool takeAlternative)
533 {
534 auto entry = active(node);
535 if (!entry) return std::unexpected(entry.error());
536 if (!(*entry)->runtime.changeAlternative(
537 decisionFrame, takeAlternative))
538 return std::unexpected(
539 "video alternative decision frame is not configured");
540 return {};
541 }
542 std::expected<void, std::string> SequenceRuntimeBridge::forgetAlternatives(
544 {
545 auto entry = active(node);
546 if (!entry) return std::unexpected(entry.error());
547 (*entry)->runtime.forgetAlternatives();
548 return {};
549 }
550
551 std::expected<void, std::string> SequenceRuntimeBridge::cutToFrame(
553 std::int32_t frame)
554 {
555 auto entry = active(node);
556 if (!entry) return std::unexpected(entry.error());
557 const auto cut = (*entry)->runtime.cutToFrame(frame);
558 if (!cut) return cut;
559 if ((*entry)->presentation)
560 {
561 (*entry)->requestedSeek = static_cast<std::uint64_t>(frame) *
562 (*entry)->runtime.metadata().microsecondsPerFrame;
563 (*entry)->seekPending = true;
564 }
565 return {};
566 }
567
569 {
570 active_.clear();
571 states_.clear();
572 }
573}
LegacyBitmapMetadata metadata
LegacyBitmapRGBA8 image
std::vector< std::byte > bytes
bool published
Definition Engine.cpp:170
std::uint32_t height
std::uint32_t width
unsigned index
std::size_t frame
std::uint16_t pitch
std::string path
std::string error
std::expected< void, std::string > forgetAlternatives(sequence::SequenceNodeId node)
std::expected< std::vector< SequenceJumpNotice >, std::string > sync(engine::SequencePlayback &playback)
std::expected< void, std::string > setAlternative(sequence::SequenceNodeId node, std::int32_t decisionFrame, std::int32_t jumpToFrame)
std::expected< void, std::string > chooseAlternative(sequence::SequenceNodeId node, std::int32_t decisionFrame, bool takeAlternative)
std::expected< void, std::string > cutToFrame(sequence::SequenceNodeId node, std::int32_t frame)
std::uint64_t SequenceNodeId
constexpr std::uint32_t BasicClockRateHz
Definition Timers.hpp:9
std::size_t size()
std::expected< sequence::Matrix2D, std::string > videoFrameTransform(std::uint32_t width, std::uint32_t height, const data::Sequence2DBoundingBoxAttribute &bounds, const sequence::Matrix2D &world)
std::expected< data::LegacyBitmapRGBA8, std::string > prepareVideoFrame(DecodedVideoFrame frame, const data::SequenceVideoData &options)
data::Sequence2DBoundingBoxAttribute binkDoubleSizeBounds(std::uint32_t width, std::uint32_t height) noexcept