diff --git a/CHANGELOG b/CHANGELOG index 56ee1790e..0f6f6dbb0 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -15,6 +15,15 @@ API Changes LEGACY_SPRITE_ARGS = true to their games.dat entry. See "Migrating from Singe 2.10" in the manual. +- Singe is no longer laserdisc only. A game declares a disc with + DISC = true in games.dat (or --disc / --framefile on the command line); + otherwise it runs without one, on a canvas sized by CANVAS_X/CANVAS_Y or + --canvas=WxH (default 720x480), the disc functions become harmless, and + discGetState() returns the new DISC_EJECTED. + Every existing games.dat entry needs DISC = true added; the menu skips + an entry with VIDEO but no DISC = true and says why, and the engine + refuses one if launched directly. Scripts can test SINGE_DISC. + - mouseSetEnabled(bool) and singeSetPauseKeyEnabled(bool) added. The old mouseEnable/mouseDisable and singeEnablePauseKey/singeDisablePauseKey are now aliases defined in Framework.singe. @@ -44,6 +53,13 @@ API Changes pause, quit, screenshot, or grab now work in MODE_FULL too; keyboard mappings stay raw there. +- Video decoding runs on its own thread per video, so a discSearch() far + from a keyframe no longer stalls the game loop; the previous frame stays + on screen until the new one is ready. The laserdisc and framefile videos + are handed to the GPU as YUV and converted there instead of on the CPU, + and each video reports its keyframe spacing when loaded, with a warning + when seeks may be slow. + - Constants that used to be duplicated in Framework.singe (SWITCH_*, FONT_QUALITY_*, MODE_*, MOUSE_*, OVERLAY_*, RENDER_*, SOUND_ERROR_*) are now defined by the engine. New: DISC_STOPPED/DISC_PLAYING/DISC_PAUSED diff --git a/assets/Menu.singe b/assets/Menu.singe index 85976dd81..4794f53f0 100644 --- a/assets/Menu.singe +++ b/assets/Menu.singe @@ -301,10 +301,20 @@ for dir in lfs.dir(".") do for file in lfs.dir(dir .. "/.") do if file == "games.dat" then -- Load games.dat + GAMES = {} dofile(dir .. "/games.dat") - for key,value in pairs(GAMES) do - table.insert(GAME_LIST, value) - GAME_COUNT = GAME_COUNT + 1 + for _, value in pairs(GAMES or {}) do + -- Since 2.20 a laserdisc game must say DISC = true; refuse the ambiguous cases here + -- with a message rather than letting the engine stop the menu when it is picked. + local title = tostring(value.TITLE or value.SCRIPT or "?") + if value.VIDEO and not value.DISC then + debugPrint(dir .. "/games.dat: \"" .. title .. "\" names a VIDEO without DISC = true; add DISC = true to the entry. Skipped.") + elseif value.DISC and not value.VIDEO then + debugPrint(dir .. "/games.dat: \"" .. title .. "\" says DISC = true but has no VIDEO. Skipped.") + else + table.insert(GAME_LIST, value) + GAME_COUNT = GAME_COUNT + 1 + end end GAMES = {} end diff --git a/docs/Manual.adoc b/docs/Manual.adoc index 8ff6a6635..926c5f253 100644 --- a/docs/Manual.adoc +++ b/docs/Manual.adoc @@ -107,15 +107,17 @@ Singe [OPTIONS] scriptName{.singe} The script name is the only required argument. It may be a `.singe` file, or a directory containing a script of the same name (`ActionMax` finds -`ActionMax/ActionMax.singe`). When no `--framefile` is given, Singe looks for -a video next to the script with the same base name and any extension FFmpeg -can demux, then for a `.txt` framefile. +`ActionMax/ActionMax.singe`). For a laserdisc game (`--disc`) with no +`--framefile`, Singe looks for a video next to the script with the same base +name and any extension FFmpeg can demux, then for a `.txt` framefile. [cols="1,2",options="header"] |=== | Option | Purpose | `-a`, `--aspect=N:D` | Force the aspect ratio used to pick a window size (`4:3`, `16:9`, `16:10`). | `-c`, `--showcalculated` | Print the frame ranges of every segment of a framefile, for debugging. +| `-C`, `--canvas=WxH` | World size for a game without a disc, default 720x480. Ignored when there is a disc. +| `-D`, `--disc` | Play a laserdisc video: the one named by `--framefile`, or the video found next to the script. Implied by `--framefile`. Without it a video next to the script is reported and ignored. | `-d`, `--datadir=PATHNAME` | Directory for everything Singe writes: video indexes, `trace.txt`, screenshots, the menu's `menu.dat`. A subdirectory named for the game's directory is created inside it. Defaults to the game's own directory. | `-e`, `--volume_nonvldp=PERCENT` | Sound effect and extra video volume, `0` to `100`. | `-f`, `--fullscreen` | Exclusive full screen at the desktop resolution. @@ -533,6 +535,7 @@ GAMES = { { TITLE = ".38 Ambush Alley", SCRIPT = "ActionMax/38AmbushAlley.singe", + DISC = true, VIDEO = "ActionMax/frame_38AmbushAlley.txt", DATA = "ActionMax", STRETCH = false, @@ -558,14 +561,38 @@ GAMES = { } ---- -The keys `SCRIPT`, `VIDEO`, `STRETCH`, `NO_MOUSE`, `RESOLUTION_X`, -`RESOLUTION_Y`, `SINDEN_GUN`, `AUDIO_TRACK`, and `LEGACY_SPRITE_ARGS` are -read by the engine when the menu (or your own script, through -`scriptExecute` / `scriptPush`) launches the entry; they override the -command line. `LEGACY_SPRITE_ARGS = true` runs a game written for Singe 2.10 +The keys `SCRIPT`, `DISC`, `VIDEO`, `CANVAS_X`, `CANVAS_Y`, `STRETCH`, +`NO_MOUSE`, `RESOLUTION_X`, `RESOLUTION_Y`, `SINDEN_GUN`, `AUDIO_TRACK`, and +`LEGACY_SPRITE_ARGS` are read by the engine when the menu (or your own +script, through `scriptExecute` / `scriptPush`) launches the entry; they +override the command line. A laserdisc game must say `DISC = true` and name +its `VIDEO`; an entry with a `VIDEO` but no `DISC = true` is refused with a +message, as is `DISC = true` without a `VIDEO`. A game without a disc leaves +both out and may set `CANVAS_X` / `CANVAS_Y` (default 720x480); see +<>. `LEGACY_SPRITE_ARGS = true` runs a game written for Singe 2.10 with the old sprite argument order (see <>). The remaining keys are read by the menu for display. +[#withoutadisc] +=== Games Without a Disc + +Singe no longer assumes a laserdisc. A game is a laserdisc game only when it +says so: `DISC = true` in its `games.dat` entry, or `--disc` (or a +`--framefile`) on the command line. Everything else runs without a disc, and +the world is a *canvas* instead of a video frame: `CANVAS_X` / `CANVAS_Y` in +`games.dat` or `--canvas=WxH` on the command line, 720x480 by default so a +discless game looks like a standard definition laserdisc game unless it +asks otherwise. + +Without a disc the engine draws on black, the overlay defaults to half the +canvas as usual, and the `disc*` and `vldp*` functions become harmless: +`discGetState` reports `DISC_EJECTED`, `discGetFrame` returns `0`, +`discGetWidth` / `discGetHeight` return the canvas, `vldpGetPixel` returns +black, and the transport calls trace and do nothing. Everything else -- the +overlay, sprites, fonts, sounds, extra videos through `videoLoad`, input, +and both programming models -- is identical. The global `SINGE_DISC` tells a +script which kind of game it is running as. + [#migrating] === Migrating from Singe 2.10 @@ -608,6 +635,11 @@ changed shape, but a few behaviors did: * `mouseEnable` / `mouseDisable` and `singeEnablePauseKey` / `singeDisablePauseKey` still work, as aliases of `mouseSetEnabled` and `singeSetPauseKeyEnabled`. +* A laserdisc game must now declare itself: add `DISC = true` next to + `VIDEO` in every `games.dat` entry, and pass `--disc` (or `--framefile`) + on the command line. Without it the game runs without a disc, and a + `games.dat` entry that names a `VIDEO` without `DISC = true` is refused + with a message naming the fix. See <>. * The pause key now pauses the whole game, not just the media: the script is frozen until the key is pressed again, and `SWITCH_PAUSE` is delivered to the script only when the key has been disabled. It acts on the key @@ -627,11 +659,12 @@ available to `controls.cfg` and to `Framework.singe` alike: | `MOUSE_SINGLE`, `MOUSE_MANY` (also `SINGLE_MOUSE`, `MANY_MOUSE`) | Arguments for `mouseSetMode`. | `OVERLAY_NOT_UPDATED`, `OVERLAY_UPDATED` | Return values for `onOverlayUpdate`. | `RENDER_PIXELATED`, `RENDER_SMOOTH` | Arguments for `spriteQuality` / `videoQuality`. -| `DISC_STOPPED`, `DISC_PLAYING`, `DISC_PAUSED` | Return values of `discGetState`. +| `DISC_STOPPED`, `DISC_PLAYING`, `DISC_PAUSED`, `DISC_EJECTED` | Return values of `discGetState`. | `SOUND_ERROR_INVALID`, `SOUND_REMOVE_HANDLE` | `-1`, what `soundPlay` returns when no channel is free. | `SINGE_VERSION_MAJOR`, `SINGE_VERSION_MINOR`, `SINGE_VERSION_STRING`, `SINGE_FRAMEWORK_VERSION` | The engine version, as integers, as a string (`"v2.20"`), and as the number `singeVersion()` returns. | `SINGE_DEAD_ZONE` | The `DEAD_ZONE` from `controls.cfg`. | `SINGE_LEGACY_SPRITE_ARGS` | True when the game asked for the 2.10 sprite argument order. +| `SINGE_DISC` | True when the game has a laserdisc; false when the canvas is the world. | `SINGE_GAMEPAD_BASE`, `SINGE_GAMEPAD_STRIDE`, `SINGE_AXIS_STRIDE`, `SINGE_GAMEPAD_BUTTON_OFFSET`, `SINGE_MOUSE_BASE`, `SINGE_MOUSE_STRIDE`, `SINGE_MAX_CONTROLLERS`, `SINGE_MAX_MICE` | Layout of the controller and mouse input codes; `Framework.singe` builds the `GAMEPAD_N` and `MOUSE_N` tables from them. |=== @@ -671,7 +704,12 @@ format, channel count, and rate. The first time a video is opened, Singe indexes it and stores the index next to the game's other data (`.index`). Indexing takes a while for large -files and happens again if the video changes. +files and happens again if the video changes. When a video is loaded, Singe +reports its keyframe spacing in the program trace and prints a warning if +keyframes are more than two seconds apart, because a seek has to decode +forward from the previous keyframe. Decoding happens on a separate thread, +so a slow seek shows the previous frame a little longer instead of stalling +the game. For laserdisc footage the constraints are frame accuracy and seek speed, not compression. H.264 in MP4 or MKV with a short keyframe interval (one or two @@ -1045,6 +1083,7 @@ Returns an integer describing the playback state of the disc. | `DISC_STOPPED` | `2` | Stopped (`discStop` was called) | `DISC_PLAYING` | `3` | Playing | `DISC_PAUSED` | `4` | Paused +| `DISC_EJECTED` | `5` | The game has no disc (see <>) |=== Singe's lightweight player has no distinct searching state; a disc that is seeking reports `DISC_PAUSED`. diff --git a/patches/README b/patches/README index 3aa8a6a3f..9b69bb78a 100644 --- a/patches/README +++ b/patches/README @@ -13,6 +13,14 @@ ActionMax/Emulator.singe lengthIntro, lengthGame, lengthMenu, highThreshold, lowThreshold, sensorX, sensorY, sensorLeft, and sensorTop before loading it. + The ActionMax games.dat is not part of this repository. Since Singe + 2.20 every laserdisc entry in it needs one added line, next to VIDEO: + + DISC = true, + + Entries without it are skipped by the menu with a message, and an entry + that names a VIDEO without DISC = true is refused by the engine. + daitarn_3_singe/Script/toolbox.singe Helper library from "Daitarn 3" (Karis, 2020). The calling script must define OVLW, OVLH, and bPause. diff --git a/src/frameFile.c b/src/frameFile.c index a0597df42..d81322bf5 100644 --- a/src/frameFile.c +++ b/src/frameFile.c @@ -216,7 +216,7 @@ int32_t frameFileLoad(const char *filename, const char *indexPath, SDL_Renderer audio = NULL; } } - files[count].videoHandle = videoLoad(files[count].filename, audio, indexPath, renderer); + files[count].videoHandle = videoLoad(files[count].filename, audio, indexPath, renderer, false); free(audio); count++; } diff --git a/src/main.c b/src/main.c index fa927d362..81bde568b 100644 --- a/src/main.c +++ b/src/main.c @@ -124,6 +124,8 @@ static const OptionT _options[] = { { 'a', "aspect", ap_yes, "N:D", "force aspect ratio", false }, { 'b', "scalefactor", ap_yes, "PERCENT", "reduce screen size for overscan compensation", true }, { 'c', "showcalculated", ap_no, NULL, "show calculated framefile values for debugging", false }, + { 'C', "canvas", ap_yes, "WxH", "world size for games without a disc (default 720x480)", false }, + { 'D', "disc", ap_no, NULL, "play a laserdisc video (implied by --framefile)", false }, { 'd', "datadir", ap_yes, "PATHNAME", "alternate location for written files", false }, { 'e', "volume_nonvldp", ap_yes, "PERCENT", "specify sound effects volume in percent", false }, { 'f', "fullscreen", ap_no, NULL, "run in full screen mode", false }, @@ -540,6 +542,7 @@ static ConfigT *_parseArguments(const char *exeName, int32_t argc, char *argv[]) int32_t aspectDom = -1; int32_t *target = NULL; char *aspectString = NULL; + char *canvasString = NULL; char *sindenString = NULL; char *temp = NULL; const char *arg = NULL; @@ -574,6 +577,8 @@ static ConfigT *_parseArguments(const char *exeName, int32_t argc, char *argv[]) conf->volumeNonVldp = VOLUME_MAX; conf->scaleFactor = SCALE_FACTOR_MAX; conf->resolutionWasCalculated = true; + conf->canvasWidth = CANVAS_DEFAULT_WIDTH; + conf->canvasHeight = CANVAS_DEFAULT_HEIGHT; // Parse command line for (argIndex = 0; argIndex < ap_arguments(&parser); argIndex++) { @@ -608,6 +613,17 @@ static ConfigT *_parseArguments(const char *exeName, int32_t argc, char *argv[]) conf->showCalculated = true; break; + // Canvas size + case 'C': + free(canvasString); + canvasString = strdup(arg); + break; + + // Laserdisc + case 'D': + conf->disc = true; + break; + // Data Dir case 'd': free(conf->dataDir); @@ -680,10 +696,11 @@ static ConfigT *_parseArguments(const char *exeName, int32_t argc, char *argv[]) conf->stretchVideo = true; break; - // Video File + // Video File - a video means a disc. case 'v': free(conf->videoFile); conf->videoFile = strdup(arg); + conf->disc = true; break; // Full Screen Windowed @@ -778,6 +795,19 @@ static ConfigT *_parseArguments(const char *exeName, int32_t argc, char *argv[]) free(aspectString); } + // Did they specify a canvas size? + if (canvasString) { + temp = strchr(canvasString, 'x'); + if (temp == NULL) { + _showUsage(exeName, "Canvas size must be WIDTHxHEIGHT, for example 640x480."); + } + *temp = 0; + if (!_parseInteger(canvasString, &conf->canvasWidth) || !_parseInteger(temp + 1, &conf->canvasHeight) || (conf->canvasWidth <= 0) || (conf->canvasHeight <= 0)) { + _showUsage(exeName, "Canvas size must be WIDTHxHEIGHT, for example 640x480."); + } + free(canvasString); + } + return conf; } @@ -849,11 +879,17 @@ static void _resolveFiles(const char *exeName, ConfigT *conf) { } } free(temp); + // A game is only a laserdisc game when it says so. + if (!conf->disc && conf->videoFile) { + utilSay("Note: %s found but --disc was not given; running without a disc.", conf->videoFile); + free(conf->videoFile); + conf->videoFile = NULL; + } } - if (!conf->videoFile) { + if (conf->disc && !conf->videoFile) { _showUsage(exeName, "Unable to locate video."); } - conf->isFrameFile = isFrameFileName(conf->videoFile); + conf->isFrameFile = conf->disc && isFrameFileName(conf->videoFile); if (conf->dataDir) { // They provided a data directory. Append the game name. diff --git a/src/main.h b/src/main.h index 41e2736c1..c06971cf2 100644 --- a/src/main.h +++ b/src/main.h @@ -33,6 +33,8 @@ #define SCALE_FACTOR_MIN 50 #define SCALE_FACTOR_MAX 100 #define DIRECTORY_MODE 0777 +#define CANVAS_DEFAULT_WIDTH 720 +#define CANVAS_DEFAULT_HEIGHT 480 ConfigT *cloneConf(const ConfigT *conf); diff --git a/src/singe.c b/src/singe.c index dcfdd7d55..2f7b9be1e 100644 --- a/src/singe.c +++ b/src/singe.c @@ -139,7 +139,8 @@ typedef enum MouseModeE { typedef enum DiscStateE { DISC_STOPPED = 2, DISC_PLAYING = 3, - DISC_PAUSED = 4 + DISC_PAUSED = 4, + DISC_EJECTED = 5 // The game has no disc at all } DiscStateE; typedef enum FontQualityE { @@ -290,7 +291,9 @@ typedef struct GlobalS { int32_t keyboardLastDown; int32_t keyboardLastUp; int32_t frameFileHandle; - int32_t videoHandle; + int32_t videoHandle; // -1 when the game has no disc + int32_t canvasWidth; // World size: the disc's, or the configured canvas + int32_t canvasHeight; FontQualityE fontQuality; MouseModeE mouseMode; int32_t mouseCount; @@ -718,8 +721,12 @@ static ConfigT *_buildConfFromTable(lua_State *L) { int64_t valueNumber = 0; ConfigT *c = NULL; - // Start with current config. + // Start with current config, but every entry declares its own disc. c = cloneConf(_global.conf); + c->disc = false; + c->isFrameFile = false; + free(c->videoFile); + c->videoFile = NULL; // Update with data in the table on the top of the Lua stack. lua_pushnil(L); @@ -784,12 +791,29 @@ static ConfigT *_buildConfFromTable(lua_State *L) { c->audioOutputTrack = (int32_t)valueNumber; } else if (strcmp(confKey, "LEGACY_SPRITE_ARGS") == 0) { c->legacySpriteArgs = valueBoolean; + } else if (strcmp(confKey, "DISC") == 0) { + c->disc = valueBoolean; + } else if (strcmp(confKey, "CANVAS_X") == 0) { + c->canvasWidth = (int32_t)valueNumber; + } else if (strcmp(confKey, "CANVAS_Y") == 0) { + c->canvasHeight = (int32_t)valueNumber; } // Clean up for next pair lua_pop(L, 1); } + // A laserdisc game says DISC = true and names its VIDEO; anything else is a mistake worth stopping for. + if (c->disc && (c->videoFile == NULL)) { + utilDie("%s: DISC = true but no VIDEO given.", c->scriptFile); + } + if (!c->disc && (c->videoFile != NULL)) { + utilDie("%s: VIDEO given without DISC = true. Add DISC = true to the games.dat entry for a laserdisc game.", c->scriptFile); + } + if ((c->canvasWidth <= 0) || (c->canvasHeight <= 0)) { + utilDie("%s: CANVAS_X and CANVAS_Y must be positive.", c->scriptFile); + } + // Create new data dir location based on script location. free(c->dataDir); c->dataDir = createDataDir(c->dataDirBase, c->scriptFile); @@ -1591,6 +1615,8 @@ static void _pushConstants(lua_State *L) { lua_setglobal(L, "DISC_PLAYING"); lua_pushinteger(L, DISC_PAUSED); lua_setglobal(L, "DISC_PAUSED"); + lua_pushinteger(L, DISC_EJECTED); + lua_setglobal(L, "DISC_EJECTED"); lua_pushinteger(L, -1); lua_setglobal(L, "SOUND_ERROR_INVALID"); @@ -1628,6 +1654,8 @@ static void _pushConstants(lua_State *L) { lua_setglobal(L, "SINGE_DEAD_ZONE"); lua_pushboolean(L, _global.conf->legacySpriteArgs); lua_setglobal(L, "SINGE_LEGACY_SPRITE_ARGS"); + lua_pushboolean(L, _global.conf->disc); + lua_setglobal(L, "SINGE_DISC"); } @@ -1918,7 +1946,7 @@ static void _takeScreenshot(void) { static void _updatePauseState(void) { if (_global.pauseState) { // Pause laserdisc - if (!_global.discStopped && videoIsPlaying(_global.videoHandle)) { + if ((_global.videoHandle >= 0) && !_global.discStopped && videoIsPlaying(_global.videoHandle)) { _global.wasPlayingBeforePause = true; videoPause(_global.videoHandle); } @@ -1926,7 +1954,7 @@ static void _updatePauseState(void) { Mix_Pause(-1); } else { // Resume laserdisc - if (!_global.discStopped && _global.wasPlayingBeforePause) { + if ((_global.videoHandle >= 0) && !_global.discStopped && _global.wasPlayingBeforePause) { _global.wasPlayingBeforePause = false; videoPlay(_global.videoHandle); } @@ -2110,9 +2138,7 @@ static int32_t apiDiscGetFrame(lua_State *L) { static int32_t apiDiscGetHeight(lua_State *L) { int32_t height = 0; - if (_global.videoHandle >= 0) { - height = videoGetHeight(_global.videoHandle); - } + height = _global.canvasHeight; _luaTrace(L, "discGetHeight", "%d", height); lua_pushinteger(L, height); @@ -2140,11 +2166,13 @@ static int32_t apiDiscGetLanguage(lua_State *L) { } -// state = discGetState() One of DISC_STOPPED, DISC_PLAYING, DISC_PAUSED. +// state = discGetState() One of DISC_STOPPED, DISC_PLAYING, DISC_PAUSED, or DISC_EJECTED (no disc). static int32_t apiDiscGetState(lua_State *L) { DiscStateE state = DISC_PAUSED; - if (_global.discStopped) { + if (_global.videoHandle < 0) { + state = DISC_EJECTED; + } else if (_global.discStopped) { state = DISC_STOPPED; } else { if ((_global.videoHandle >= 0) && videoIsPlaying(_global.videoHandle)) { @@ -2162,9 +2190,7 @@ static int32_t apiDiscGetState(lua_State *L) { static int32_t apiDiscGetWidth(lua_State *L) { int32_t width = 0; - if (_global.videoHandle >= 0) { - width = videoGetWidth(_global.videoHandle); - } + width = _global.canvasWidth; _luaTrace(L, "discGetWidth", "%d", width); lua_pushinteger(L, width); @@ -2897,10 +2923,8 @@ static int32_t apiOverlaySetResolution(lua_State *L) { utilDie("%s", SDL_GetError()); } SDL_SetTextureBlendMode(_global.overlayTexture, SDL_BLENDMODE_BLEND); - if (_global.videoHandle >= 0) { - _global.overlayScaleX = (double)width / (double)videoGetWidth(_global.videoHandle); - _global.overlayScaleY = (double)height / (double)videoGetHeight(_global.videoHandle); - } + _global.overlayScaleX = (double)width / (double)_global.canvasWidth; + _global.overlayScaleY = (double)height / (double)_global.canvasHeight; _overlayTouched(); _luaTrace(L, "overlaySetResolution", "%d %d", width, height); @@ -3827,7 +3851,7 @@ static int32_t apiVideoLoad(lua_State *L) { if (!video) { _luaDie(L, "videoLoad", "Unable to allocate new video."); } - video->handle = videoLoad(name, NULL, dataDir, _global.renderer); + video->handle = videoLoad(name, NULL, dataDir, _global.renderer, true); video->id = _global.nextVideoId++; video->lastFrame = -1; video->scaleX = 1.0; @@ -4327,21 +4351,29 @@ void singe(SDL_Window *window, SDL_Renderer *renderer, ConfigT *conf) { lua_register(_global.luaContext, "vldpGetWidth", apiDiscGetWidth); // 1.xx Same as discGetWidth. lua_register(_global.luaContext, "vldpSetVerbose", apiVldpSetVerbose); // 1.xx - // Open main video file - _progTrace("Opening main video file"); - _doIndexDisplay(INDEX_DISPLAY_START); - videoSetIndexCallback(_doIndexDisplay); - if (_global.conf->isFrameFile) { - _global.frameFileHandle = frameFileLoad(_global.conf->videoFile, _global.conf->dataDir, _global.renderer, _global.conf->showCalculated); - frameFileSeek(_global.frameFileHandle, 0, &_global.videoHandle, &thisFrame); // Fills in _global.videoHandle + // Open main video file, if this is a laserdisc game. Otherwise the canvas is the world. + if (_global.conf->disc) { + _progTrace("Opening main video file"); + _doIndexDisplay(INDEX_DISPLAY_START); + videoSetIndexCallback(_doIndexDisplay); + if (_global.conf->isFrameFile) { + _global.frameFileHandle = frameFileLoad(_global.conf->videoFile, _global.conf->dataDir, _global.renderer, _global.conf->showCalculated); + frameFileSeek(_global.frameFileHandle, 0, &_global.videoHandle, &thisFrame); // Fills in _global.videoHandle + } else { + _global.videoHandle = videoLoad(_global.conf->videoFile, NULL, _global.conf->dataDir, _global.renderer, false); + } + videoSetVolume(_global.videoHandle, _global.conf->volumeVldp, _global.conf->volumeVldp); + videoSetIndexCallback(NULL); + _doIndexDisplay(INDEX_DISPLAY_STOP); + _global.canvasWidth = videoGetWidth(_global.videoHandle); + _global.canvasHeight = videoGetHeight(_global.videoHandle); } else { - _global.videoHandle = videoLoad(_global.conf->videoFile, NULL, _global.conf->dataDir, _global.renderer); + _progTrace("No disc; canvas is %dx%d", _global.conf->canvasWidth, _global.conf->canvasHeight); + _global.canvasWidth = _global.conf->canvasWidth; + _global.canvasHeight = _global.conf->canvasHeight; } - videoSetVolume(_global.videoHandle, _global.conf->volumeVldp, _global.conf->volumeVldp); - videoSetIndexCallback(NULL); - _doIndexDisplay(INDEX_DISPLAY_STOP); - videoWidth = videoGetWidth(_global.videoHandle); - videoHeight = videoGetHeight(_global.videoHandle); + videoWidth = _global.canvasWidth; + videoHeight = _global.canvasHeight; // Should we resize the window to the video's shape? if (conf->resolutionWasCalculated && !conf->fullScreen && !conf->fullScreenWindow) { @@ -4507,11 +4539,13 @@ void singe(SDL_Window *window, SDL_Renderer *renderer, ConfigT *conf) { SDL_SetColorKey(_global.consoleFontSurface, SDL_TRUE, _global.consoleFontSurface->format->Rmask | _global.consoleFontSurface->format->Bmask); // The disc always starts parked on frame 1, paused, like discSearch(1). - _progTrace("Parking laserdisc on frame 1"); - _discSeek(1); - videoPause(_global.videoHandle); - _global.discStopped = false; - _selectDefaultAudioTrack(_global.videoHandle); + if (_global.videoHandle >= 0) { + _progTrace("Parking laserdisc on frame 1"); + _discSeek(1); + videoPause(_global.videoHandle); + _global.discStopped = false; + _selectDefaultAudioTrack(_global.videoHandle); + } // Start script _progTrace("Running %s", _global.conf->scriptFile); @@ -4696,16 +4730,18 @@ void singe(SDL_Window *window, SDL_Renderer *renderer, ConfigT *conf) { } } - // Update video - thisFrame = videoUpdate(_global.videoHandle, &_global.videoTexture); - if (_global.conf->isFrameFile) { - frameFileUpdate(_global.frameFileHandle, &_global.videoHandle); - } - // Did we get a new video frame? - if (thisFrame != lastFrame) { - lastFrame = thisFrame; - frameClock = 0; - _global.refreshDisplay = true; + // Update the disc video + if (_global.videoHandle >= 0) { + thisFrame = videoUpdate(_global.videoHandle, &_global.videoTexture); + if (_global.conf->isFrameFile) { + frameFileUpdate(_global.frameFileHandle, &_global.videoHandle); + } + // Did we get a new video frame? + if (thisFrame != lastFrame) { + lastFrame = thisFrame; + frameClock = 0; + _global.refreshDisplay = true; + } } // Call game code, unless the engine has it paused. @@ -4735,13 +4771,15 @@ void singe(SDL_Window *window, SDL_Renderer *renderer, ConfigT *conf) { SDL_SetRenderDrawColor(_global.renderer, sindenWhiteColor.r, sindenWhiteColor.g, sindenWhiteColor.b, sindenWhiteColor.a); SDL_RenderFillRect(_global.renderer, &sindenWhite); } - // Laserdisc Video - if (_global.discStopped) { - // Stopped discs display blue like the good old days - SDL_SetRenderDrawColor(_global.renderer, 0, 0, BLUE_SCREEN_BLUE, SDL_ALPHA_OPAQUE); - SDL_RenderFillRect(_global.renderer, &windowTarget); - } else { - SDL_RenderCopy(_global.renderer, _global.videoTexture, NULL, &windowTarget); + // Laserdisc Video. Games without a disc draw on black. + if (_global.videoHandle >= 0) { + if (_global.discStopped) { + // Stopped discs display blue like the good old days + SDL_SetRenderDrawColor(_global.renderer, 0, 0, BLUE_SCREEN_BLUE, SDL_ALPHA_OPAQUE); + SDL_RenderFillRect(_global.renderer, &windowTarget); + } else { + SDL_RenderCopy(_global.renderer, _global.videoTexture, NULL, &windowTarget); + } } // Overlay if (_global.overlayDirty) { @@ -4806,11 +4844,14 @@ void singe(SDL_Window *window, SDL_Renderer *renderer, ConfigT *conf) { } // Unload background video - _progTrace("Unloading main video file"); if (_global.conf->isFrameFile) { + _progTrace("Unloading framefile"); frameFileUnload(_global.frameFileHandle); } else { - videoUnload(_global.videoHandle); + if (_global.videoHandle >= 0) { + _progTrace("Unloading main video file"); + videoUnload(_global.videoHandle); + } } // Stop controllers diff --git a/src/singe.h b/src/singe.h index 41e12aac8..c53557aef 100644 --- a/src/singe.h +++ b/src/singe.h @@ -63,12 +63,15 @@ typedef struct ConfigS { bool programTracing; bool scriptTracing; bool legacySpriteArgs; + bool disc; // Play a laserdisc video; otherwise the canvas is the world int32_t bestRatioIndex; int32_t volumeVldp; int32_t volumeNonVldp; int32_t scaleFactor; int32_t xResolution; int32_t yResolution; + int32_t canvasWidth; // World size when there is no disc + int32_t canvasHeight; int32_t sindenArgc; int32_t sindenArgv[SINDEN_ARG_MAX]; int32_t audioOutputTrack; diff --git a/src/videoPlayer.c b/src/videoPlayer.c index 3829bd271..820a1fe44 100644 --- a/src/videoPlayer.c +++ b/src/videoPlayer.c @@ -55,8 +55,17 @@ typedef struct iso639_lang_t iso639_lang_t; #define MS_PER_SECOND 1000.0 #define PERCENT_TO_SCALE 0.01f #define PERCENT_MAX 100 +#define PLANE_COUNT 3 // Y, U, V +#define KEYFRAME_WARN_SECONDS 2.0 // Seeks decode forward from the previous keyframe +// One decoded frame owned by the player (FFMS2 reuses its own buffer, so we copy). +typedef struct FrameBufferS { + uint8_t *data[PLANE_COUNT]; + int32_t linesize[PLANE_COUNT]; + int64_t frame; // -1 until something has been decoded into it +} FrameBufferT; + typedef struct AudioStreamS { FFMS_AudioSource *audioSource; const FFMS_AudioProperties *audioProps; @@ -69,7 +78,6 @@ typedef struct VideoPlayerS { int32_t id; bool playing; bool resetTime; - bool frameDirty; uint8_t *audioBuffer; uint8_t *audioSilenceRaw; uint8_t audioSampleBytes; @@ -102,21 +110,40 @@ typedef struct VideoPlayerS { FFMS_Track *videoTrackHandle; const FFMS_VideoProperties *videoProps; const FFMS_TrackTimeBase *videoTimeBase; - const FFMS_Frame *frameData; + bool rgb; // BGRA frames for scripts to read; otherwise YUV for the GPU + FrameBufferT front; // What the texture and the script see + FrameBufferT back; // What the decoder thread fills + bool backReady; + bool threadError; + bool quitThread; + int64_t requestedFrame; // Waiting for the decoder, -1 when none + int64_t pendingFrame; // Last frame asked for, so it is not asked for twice + int64_t uploadedFrame; // Frame currently on the texture + SDL_Thread *thread; + SDL_mutex *lock; + SDL_cond *wake; + FFMS_ErrorInfo threadErrInfo; + char threadErrMsg[ERROR_BUFFER_SIZE]; UT_hash_handle hh; } VideoPlayerT; #pragma GCC diagnostic pop static int64_t _audioClock(VideoPlayerT *v, uint32_t now); +static void _allocateFrameBuffer(VideoPlayerT *v, FrameBufferT *buffer); +static void _copyFrame(VideoPlayerT *v, FrameBufferT *buffer, const FFMS_Frame *frame); static FFMS_Index *_createIndex(const char *filename, const char *indexPath, bool hasVideo, bool hasAudio, VideoPlayerT *v); +static int _decoderThread(void *data); // SDL thread entry. Not changing int. static void _dequeueVideoAudio(int channel, void *stream, int bytes, void *udata); // Callback. Not changing ints. static void _feedAudio(VideoPlayerT *v); static int64_t _frameTime(VideoPlayerT *v, int64_t frame); static VideoPlayerT *_getPlayer(int32_t playerHandle, const char *caller); static int FFMS_CC _indexCallBack(int64_t current, int64_t total, void *icPrivate); // Callback. Not changing int. static void _loadAudio(VideoPlayerT *v, const char *filename, FFMS_Index *index); -static void _loadFrame(VideoPlayerT *v); +static void _reportKeyframes(VideoPlayerT *v, const char *filename); +static void _requestFrame(VideoPlayerT *v); +static bool _takeDecodedFrame(VideoPlayerT *v); +static void _uploadFrame(VideoPlayerT *v); static int64_t _msToSamples(VideoPlayerT *v, int64_t ms); static void _resetClock(VideoPlayerT *v, uint32_t now); @@ -131,6 +158,49 @@ static int64_t _mixLatencyMs = 0; // Time between handing static SDL_AudioFormat _mixFormat = 0; +static void _allocateFrameBuffer(VideoPlayerT *v, FrameBufferT *buffer) { + int32_t chromaWidth = (v->width + 1) / 2; + int32_t chromaHeight = (v->height + 1) / 2; + + buffer->frame = -1; + if (v->rgb) { + buffer->linesize[0] = v->width * BYTES_PER_PIXEL; + buffer->data[0] = malloc((size_t)buffer->linesize[0] * (size_t)v->height); + if (!buffer->data[0]) { + utilDie("Unable to allocate frame buffer."); + } + } else { + buffer->linesize[0] = v->width; + buffer->linesize[1] = chromaWidth; + buffer->linesize[2] = chromaWidth; + buffer->data[0] = malloc((size_t)v->width * (size_t)v->height); + buffer->data[1] = malloc((size_t)chromaWidth * (size_t)chromaHeight); + buffer->data[2] = malloc((size_t)chromaWidth * (size_t)chromaHeight); + if (!buffer->data[0] || !buffer->data[1] || !buffer->data[2]) { + utilDie("Unable to allocate frame buffer."); + } + } +} + + +// Copies FFMS2's frame into our buffer, one plane at a time (line sizes may differ). +static void _copyFrame(VideoPlayerT *v, FrameBufferT *buffer, const FFMS_Frame *frame) { + int32_t plane = 0; + int32_t planes = v->rgb ? 1 : PLANE_COUNT; + int32_t rows = 0; + int32_t width = 0; + int32_t y = 0; + + for (plane = 0; plane < planes; plane++) { + rows = (plane == 0) ? v->height : (v->height + 1) / 2; + width = buffer->linesize[plane]; + for (y = 0; y < rows; y++) { + memcpy(buffer->data[plane] + y * buffer->linesize[plane], frame->Data[plane] + y * frame->Linesize[plane], (size_t)width); + } + } +} + + // Presentation time (ms) the listener is hearing right now. Audio is the master clock: // the picture is fitted to what the device has actually consumed, so the two cannot drift. static int64_t _audioClock(VideoPlayerT *v, uint32_t now) { @@ -200,6 +270,43 @@ static FFMS_Index *_createIndex(const char *filename, const char *indexPath, boo } +// Decodes whatever frame was last requested into the back buffer, then waits for the next request. +// FFMS2 seeks by decoding forward from a keyframe, so this keeps long seeks off the main loop. +static int _decoderThread(void *data) { + VideoPlayerT *v = (VideoPlayerT *)data; + const FFMS_Frame *frame = NULL; + int64_t want = -1; + + SDL_LockMutex(v->lock); + while (!v->quitThread) { + // Only one frame is in flight and the main thread must take it before the next one. + if ((v->requestedFrame < 0) || v->backReady) { + SDL_CondWait(v->wake, v->lock); + continue; + } + want = v->requestedFrame; + v->requestedFrame = -1; + SDL_UnlockMutex(v->lock); + + frame = FFMS_GetFrame(v->videoSource, (int)want, &v->threadErrInfo); + if (frame != NULL) { + _copyFrame(v, &v->back, frame); + } + + SDL_LockMutex(v->lock); + if (frame == NULL) { + v->threadError = true; + } else { + v->back.frame = want; + v->backReady = true; + } + } + SDL_UnlockMutex(v->lock); + + return 0; +} + + // Runs on the SDL_mixer audio thread. Everything it touches is guarded by SDL_LockAudio on the main thread. static void _dequeueVideoAudio(int channel, void *stream, int bytes, void *udata) { VideoPlayerT *v = (VideoPlayerT *)udata; @@ -375,19 +482,50 @@ static void _loadAudio(VideoPlayerT *v, const char *filename, FFMS_Index *index) } -// Decode the current frame and push it to the texture. -static void _loadFrame(VideoPlayerT *v) { - v->frameData = FFMS_GetFrame(v->videoSource, (int)v->frame, &v->errInfo); - if (v->frameData == NULL) { - utilDie("%s", v->errInfo.Buffer); - } - SDL_UpdateTexture(v->videoTexture, NULL, v->frameData->Data[0], v->frameData->Linesize[0]); - v->frameDirty = false; +static int64_t _msToSamples(VideoPlayerT *v, int64_t ms) { + return (int64_t)((double)ms / MS_PER_SECOND * (double)v->audio[v->currentAudioTrack].audioProps->SampleRate); } -static int64_t _msToSamples(VideoPlayerT *v, int64_t ms) { - return (int64_t)((double)ms / MS_PER_SECOND * (double)v->audio[v->currentAudioTrack].audioProps->SampleRate); +// Tells the author how far a seek may have to decode. Keyframe spacing is a property of the file. +static void _reportKeyframes(VideoPlayerT *v, const char *filename) { + int32_t frame = 0; + int32_t lastKeyframe = 0; + int32_t keyframes = 0; + int32_t longestGap = 0; + double seconds = 0.0; + + for (frame = 0; frame < v->videoProps->NumFrames; frame++) { + if (FFMS_GetFrameInfo(v->videoTrackHandle, frame)->KeyFrame) { + if ((keyframes > 0) && (frame - lastKeyframe > longestGap)) { + longestGap = frame - lastKeyframe; + } + lastKeyframe = frame; + keyframes++; + } + } + if (v->videoProps->NumFrames - lastKeyframe > longestGap) { + longestGap = v->videoProps->NumFrames - lastKeyframe; + } + if (v->videoProps->FPSNumerator > 0) { + seconds = (double)longestGap * (double)v->videoProps->FPSDenominator / (double)v->videoProps->FPSNumerator; + } + utilTrace("%s: %d frames, %d keyframes, longest gap %d frames (%.1f seconds)", filename, v->videoProps->NumFrames, keyframes, longestGap, seconds); + if (seconds > KEYFRAME_WARN_SECONDS) { + utilSay("Warning: %s has keyframes up to %.1f seconds apart; seeking into that video may stall. Re-encode with a keyframe interval of two seconds or less.", filename, seconds); + } +} + + +// Asks the decoder thread for the frame the clock says we should be showing, if it is not already coming. +static void _requestFrame(VideoPlayerT *v) { + SDL_LockMutex(v->lock); + if ((v->frame != v->front.frame) && (v->frame != v->pendingFrame)) { + v->requestedFrame = v->frame; + v->pendingFrame = v->frame; + SDL_CondSignal(v->wake); + } + SDL_UnlockMutex(v->lock); } @@ -395,7 +533,6 @@ static int64_t _msToSamples(VideoPlayerT *v, int64_t ms) { static void _resetClock(VideoPlayerT *v, uint32_t now) { v->startTicks = now; v->startTime = _frameTime(v, v->frame); - v->frameDirty = true; v->resetTime = false; if (v->audioSourceCount > 0) { SDL_LockAudio(); @@ -409,6 +546,40 @@ static void _resetClock(VideoPlayerT *v, uint32_t now) { } +// Swaps in a finished frame from the decoder thread. Returns true when there is a new one. +static bool _takeDecodedFrame(VideoPlayerT *v) { + FrameBufferT temp; + bool taken = false; + + SDL_LockMutex(v->lock); + if (v->threadError) { + utilDie("%s", v->threadErrInfo.Buffer); + } + if (v->backReady) { + temp = v->front; + v->front = v->back; + v->back = temp; + v->backReady = false; + taken = true; + SDL_CondSignal(v->wake); + } + SDL_UnlockMutex(v->lock); + + return taken; +} + + +// Pushes the front buffer to the texture. YUV players convert on the GPU. +static void _uploadFrame(VideoPlayerT *v) { + if (v->rgb) { + SDL_UpdateTexture(v->videoTexture, NULL, v->front.data[0], v->front.linesize[0]); + } else { + SDL_UpdateYUVTexture(v->videoTexture, NULL, v->front.data[0], v->front.linesize[0], v->front.data[1], v->front.linesize[1], v->front.data[2], v->front.linesize[2]); + } + v->uploadedFrame = v->front.frame; +} + + int32_t videoGetAudioTrack(int32_t playerHandle) { return _getPlayer(playerHandle, "videoGetAudioTrack")->currentAudioTrack; } @@ -465,33 +636,49 @@ const char *videoGetLanguageDescription(const char *languageCode) { } -// Reads one pixel of the most recently decoded frame. Returns false if there is no frame yet. +// Reads one pixel of the frame being shown. Returns false if there is no frame yet. bool videoGetPixel(int32_t playerHandle, int32_t x, int32_t y, uint8_t *r, uint8_t *g, uint8_t *b) { VideoPlayerT *v = _getPlayer(playerHandle, "videoGetPixel"); const uint8_t *pixel = NULL; + int32_t c = 0; + int32_t d = 0; + int32_t e = 0; + int32_t value = 0; - if ((v->frameData == NULL) || (x < 0) || (y < 0) || (x >= v->width) || (y >= v->height)) { + if ((v->front.frame < 0) || (x < 0) || (y < 0) || (x >= v->width) || (y >= v->height)) { return false; } - // Frames are decoded as BGRA. - pixel = v->frameData->Data[0] + (y * v->frameData->Linesize[0]) + (x * BYTES_PER_PIXEL); - *b = pixel[0]; - *g = pixel[1]; - *r = pixel[2]; + if (v->rgb) { + pixel = v->front.data[0] + (y * v->front.linesize[0]) + (x * BYTES_PER_PIXEL); + *b = pixel[0]; + *g = pixel[1]; + *r = pixel[2]; + } else { + // BT.601 limited range, the same conversion SDL applies on the GPU for SD video. + c = v->front.data[0][y * v->front.linesize[0] + x] - 16; + d = v->front.data[1][(y / 2) * v->front.linesize[1] + (x / 2)] - 128; + e = v->front.data[2][(y / 2) * v->front.linesize[2] + (x / 2)] - 128; + value = (298 * c + 409 * e + 128) >> 8; + *r = (uint8_t)((value < 0) ? 0 : ((value > 255) ? 255 : value)); + value = (298 * c - 100 * d - 208 * e + 128) >> 8; + *g = (uint8_t)((value < 0) ? 0 : ((value > 255) ? 255 : value)); + value = (298 * c + 516 * d + 128) >> 8; + *b = (uint8_t)((value < 0) ? 0 : ((value > 255) ? 255 : value)); + } return true; } -// Exposes the most recently decoded BGRA frame. Valid until the next videoUpdate of this player. +// Exposes the BGRA frame being shown (players loaded with rgb). Valid until the next videoUpdate of this player. bool videoGetPixels(int32_t playerHandle, const uint8_t **pixels, int32_t *pitch) { VideoPlayerT *v = _getPlayer(playerHandle, "videoGetPixels"); - if (v->frameData == NULL) { + if (!v->rgb || (v->front.frame < 0)) { return false; } - *pixels = v->frameData->Data[0]; - *pitch = v->frameData->Linesize[0]; + *pixels = v->front.data[0]; + *pitch = v->front.linesize[0]; return true; } @@ -543,8 +730,9 @@ bool videoIsPlaying(int32_t playerHandle) { } -// audioFilename may be NULL when the audio lives in the video file. -int32_t videoLoad(const char *videoFilename, const char *audioFilename, const char *indexPath, SDL_Renderer *renderer) { +// audioFilename may be NULL when the audio lives in the video file. rgb players decode to BGRA so +// scripts can read the pixels; everything else stays YUV and is converted by the GPU. +int32_t videoLoad(const char *videoFilename, const char *audioFilename, const char *indexPath, SDL_Renderer *renderer, bool rgb) { int32_t pixelFormats[2]; FFMS_Index *vIndex = NULL; FFMS_Index *aIndex = NULL; @@ -568,6 +756,12 @@ int32_t videoLoad(const char *videoFilename, const char *audioFilename, const ch v->errInfo.BufferSize = sizeof(v->errMsg); v->errInfo.ErrorType = FFMS_ERROR_SUCCESS; v->errInfo.SubType = FFMS_ERROR_SUCCESS; + v->threadErrInfo = v->errInfo; + v->threadErrInfo.Buffer = v->threadErrMsg; + v->rgb = rgb; + v->requestedFrame = -1; + v->pendingFrame = -1; + v->uploadedFrame = -1; if (audioFilename) { vIndex = _createIndex(videoFilename, indexPath, true, false, v); @@ -599,12 +793,13 @@ int32_t videoLoad(const char *videoFilename, const char *audioFilename, const ch v->width = frame->EncodedWidth; v->height = frame->EncodedHeight; - // Set up output video format - pixelFormats[0] = FFMS_GetPixFmt("bgra"); + // Set up output video format. YUV 4:2:0 is what the decoder produces, so no conversion runs. + pixelFormats[0] = FFMS_GetPixFmt(rgb ? "bgra" : "yuv420p"); pixelFormats[1] = -1; if (FFMS_SetOutputFormatV2(v->videoSource, pixelFormats, v->width, v->height, FFMS_RESIZER_BICUBIC, &v->errInfo)) { utilDie("%s", v->errInfo.Buffer); } + _reportKeyframes(v, videoFilename); // Find audio track(s) _loadAudio(v, audioFilename, aIndex); @@ -616,11 +811,24 @@ int32_t videoLoad(const char *videoFilename, const char *audioFilename, const ch FFMS_DestroyIndex(vIndex); // Create video texture - v->videoTexture = SDL_CreateTexture(renderer, SDL_PIXELFORMAT_BGRA32, SDL_TEXTUREACCESS_STREAMING, v->width, v->height); + v->videoTexture = SDL_CreateTexture(renderer, rgb ? SDL_PIXELFORMAT_BGRA32 : SDL_PIXELFORMAT_IYUV, SDL_TEXTUREACCESS_STREAMING, v->width, v->height); if (v->videoTexture == NULL) { utilDie("%s", SDL_GetError()); } + // Frame buffers and the decoder thread that fills them + _allocateFrameBuffer(v, &v->front); + _allocateFrameBuffer(v, &v->back); + v->lock = SDL_CreateMutex(); + v->wake = SDL_CreateCond(); + if (!v->lock || !v->wake) { + utilDie("%s", SDL_GetError()); + } + v->thread = SDL_CreateThread(_decoderThread, "singeDecoder", v); + if (v->thread == NULL) { + utilDie("%s", SDL_GetError()); + } + // Do we have audio? if (v->audioSourceCount > 0) { // Determine audio format @@ -800,6 +1008,19 @@ void videoUnload(int32_t playerHandle) { free(v->audio); } + // Stop the decoder before touching anything it uses. + SDL_LockMutex(v->lock); + v->quitThread = true; + SDL_CondSignal(v->wake); + SDL_UnlockMutex(v->lock); + SDL_WaitThread(v->thread, NULL); + SDL_DestroyCond(v->wake); + SDL_DestroyMutex(v->lock); + for (x = 0; x < PLANE_COUNT; x++) { + free(v->front.data[x]); + free(v->back.data[x]); + } + FFMS_DestroyVideoSource(v->videoSource); SDL_DestroyTexture(v->videoTexture); @@ -833,8 +1054,7 @@ int64_t videoUpdate(int32_t playerHandle, SDL_Texture **texture) { // Advance to the last frame whose presentation time has arrived. next = v->frame + 1; while ((next < count) && (_frameTime(v, next) <= elapsed)) { - v->frame = next; - v->frameDirty = true; + v->frame = next; next++; } // Past the end of the last frame? Loop. @@ -848,8 +1068,10 @@ int64_t videoUpdate(int32_t playerHandle, SDL_Texture **texture) { } } - if (v->frameDirty) { - _loadFrame(v); + // Hand the decoder the frame we want, and show whatever it has finished. + _requestFrame(v); + if (_takeDecodedFrame(v)) { + _uploadFrame(v); } *texture = v->videoTexture; @@ -858,5 +1080,6 @@ int64_t videoUpdate(int32_t playerHandle, SDL_Texture **texture) { _feedAudio(v); } - return v->frame; + // Report what is on screen, so the caller redraws when it changes. + return v->uploadedFrame; } diff --git a/src/videoPlayer.h b/src/videoPlayer.h index a5dc9cd63..8b22889a6 100644 --- a/src/videoPlayer.h +++ b/src/videoPlayer.h @@ -49,7 +49,7 @@ void videoGetVolume(int32_t playerHandle, int32_t *leftPercent, int32_t * int32_t videoGetWidth(int32_t playerHandle); void videoInit(int32_t mixerChunkFrames); bool videoIsPlaying(int32_t playerHandle); -int32_t videoLoad(const char *videoFilename, const char *audioFilename, const char *indexPath, SDL_Renderer *renderer); +int32_t videoLoad(const char *videoFilename, const char *audioFilename, const char *indexPath, SDL_Renderer *renderer, bool rgb); void videoPause(int32_t playerHandle); void videoPlay(int32_t playerHandle); void videoQuit(void);