[Scummvm-git-logs] scummvm master -> 83b3d3a91bd2267e4601aee62e1d244f9a571f6d
fracturehill
noreply at scummvm.org
Wed Aug 23 09:18:00 UTC 2023
This automated email contains information about 3 new commits which have been
pushed to the 'scummvm' repo located at https://github.com/scummvm/scummvm .
Summary:
e761b5ae25 NANCY: Implement deferred loading for RaycastPuzzle
911a542b12 NANCY: Implement frame caching for AVF video
83b3d3a91b NANCY: Implement cel caching in ConversationCel
Commit: e761b5ae25de8a7673c937a0b9b6df047e68de04
https://github.com/scummvm/scummvm/commit/e761b5ae25de8a7673c937a0b9b6df047e68de04
Author: Kaloyan Chehlarski (strahy at outlook.com)
Date: 2023-08-23T12:15:06+03:00
Commit Message:
NANCY: Implement deferred loading for RaycastPuzzle
Added facilities for deferred loading of data that would take
far too long for a single frame. Deferred loading is
executed after the regular loop, where the system would
previously just sleep. Added deferred loading to RaycastPuzzle, which now no longer freezes the game
while initializing.
Changed paths:
engines/nancy/action/raycastpuzzle.cpp
engines/nancy/action/raycastpuzzle.h
engines/nancy/nancy.cpp
engines/nancy/nancy.h
engines/nancy/util.cpp
engines/nancy/util.h
diff --git a/engines/nancy/action/raycastpuzzle.cpp b/engines/nancy/action/raycastpuzzle.cpp
index 69a48e026c6..6bd9597570e 100644
--- a/engines/nancy/action/raycastpuzzle.cpp
+++ b/engines/nancy/action/raycastpuzzle.cpp
@@ -863,71 +863,196 @@ void RaycastLevelBuilder::validateMap() {
}
}
-void RaycastPuzzle::init() {
- _puzzleData = g_nancy->_raycastPuzzleData;
- assert(_puzzleData);
+class RaycastDeferredLoader : public DeferredLoader {
+public:
+ RaycastDeferredLoader(RaycastPuzzle &owner, uint width, uint height, uint verticalHeight) :
+ _owner(owner),
+ _builder(width, height, verticalHeight),
+ _loadState(kInitDrawSurface),
+ _x(0), _y(0),
+ _isDone(false) {}
+ virtual ~RaycastDeferredLoader() {}
+
+ bool _isDone;
+
+private:
+ bool loadInner() override;
+
+ enum State { kInitDrawSurface, kCopyData, kInitMap, kInitTables1, kInitTables2, kLoadTextures };
+
+ State _loadState;
+
+ RaycastPuzzle &_owner;
+ RaycastLevelBuilder _builder;
+
+ uint16 _x, _y;
+};
- RaycastLevelBuilder levelBuilder(_mapWidth, _mapHeight, _wallHeight);
-
- _wallMap.push_back(levelBuilder._wallMap);
- _infoMap.push_back(levelBuilder._infoMap);
- _floorMap.push_back(levelBuilder._floorMap);
- _ceilingMap.push_back(levelBuilder._ceilingMap);
- _heightMap.push_back(levelBuilder._heightMap);
- _wallLightMap.push_back(levelBuilder._wallLightMap);
- _floorCeilingLightMap.push_back(levelBuilder._floorCeilingLightMap);
-
- _mapFullWidth = levelBuilder._fullWidth;
- _mapFullHeight = levelBuilder._fullHeight;
-
- Common::Rect viewport = g_nancy->_viewportData->bounds;
- moveTo(viewport);
- _drawSurface.create(viewport.width(), viewport.height(), g_nancy->_graphicsManager->getInputPixelFormat());
- setTransparent(true);
-
- // TODO map is a debug feature, make sure to hide it
- // Also, fix the fact that it's rendered upside-down
- _map._drawSurface.create(_mapFullWidth, _mapFullHeight, g_nancy->_graphicsManager->getInputPixelFormat());
- Common::Rect mapPos(g_nancy->_bootSummary->textboxScreenPosition);
- mapPos.setWidth(_mapFullWidth * 2);
- mapPos.setHeight(_mapFullHeight * 2);
- _map.moveTo(mapPos);
- _map.init();
-
- drawMap();
-
- Common::Rect selectedBounds = _puzzleData->screenViewportSizes[_puzzleData->viewportSizeUsed];
-
- _wallCastColumnAngles.resize(g_nancy->_viewportData->screenPosition.width());
- uint center = selectedBounds.left + (selectedBounds.width() >> 1);
- for (uint i = 0; i < _wallCastColumnAngles.size(); ++i) {
- int32 &angle = _wallCastColumnAngles[i];
- angle = (int32)(atan(((float)i - (float)center) / (float)_fov) * _rotationSingleStep);
- clampRotation(angle);
+bool RaycastDeferredLoader::loadInner() {
+ switch(_loadState) {
+ case kInitDrawSurface : {
+ Common::Rect viewport = g_nancy->_viewportData->bounds;
+ _owner.moveTo(viewport);
+ _owner._drawSurface.create(viewport.width(), viewport.height(), g_nancy->_graphicsManager->getInputPixelFormat());
+ _owner.setTransparent(true);
+
+ _loadState = kCopyData;
+ break;
+ }
+ case kCopyData :
+ _owner._wallMap.swap(_builder._wallMap);
+ _owner._infoMap.swap(_builder._infoMap);
+ _owner._floorMap.swap(_builder._floorMap);
+ _owner._ceilingMap.swap(_builder._ceilingMap);
+ _owner._heightMap.swap(_builder._heightMap);
+ _owner._wallLightMap.swap(_builder._wallLightMap);
+ _owner._floorCeilingLightMap.swap(_builder._floorCeilingLightMap);
+ _owner._mapFullWidth = _builder._fullWidth;
+ _owner._mapFullHeight = _builder._fullHeight;
+
+ _loadState = kInitMap;
+ break;
+ case kInitMap : {
+ // TODO map is a debug feature, make sure to hide it
+ // Also, fix the fact that it's rendered upside-down
+ _owner._map._drawSurface.create(_owner._mapFullWidth, _owner._mapFullHeight, g_nancy->_graphicsManager->getInputPixelFormat());
+ Common::Rect mapPos(g_nancy->_bootSummary->textboxScreenPosition);
+ mapPos.setWidth(_owner._mapFullWidth * 2);
+ mapPos.setHeight(_owner._mapFullHeight * 2);
+ _owner._map.moveTo(mapPos);
+ _owner._map.init();
+ _owner.drawMap();
+
+ _loadState = kInitTables1;
+ break;
}
+ case kInitTables1 : {
+ Common::Rect selectedBounds = _owner._puzzleData->screenViewportSizes[_owner._puzzleData->viewportSizeUsed];
+
+ _owner._wallCastColumnAngles.resize(g_nancy->_viewportData->screenPosition.width());
+ uint center = selectedBounds.left + (selectedBounds.width() >> 1);
+ for (uint i = 0; i < _owner._wallCastColumnAngles.size(); ++i) {
+ int32 &angle = _owner._wallCastColumnAngles[i];
+ angle = (int32)(atan(((float)i - (float)center) / (float)_owner._fov) * _owner._rotationSingleStep);
+ clampRotation(angle);
+ }
+
+ _owner._leftmostAngle = _owner._wallCastColumnAngles[selectedBounds.left];
+ _owner._rightmostAngle = _owner._wallCastColumnAngles[selectedBounds.right];
- _leftmostAngle = _wallCastColumnAngles[selectedBounds.left];
- _rightmostAngle = _wallCastColumnAngles[selectedBounds.right];
+ _loadState = kInitTables2;
+ break;
+ }
+ case kInitTables2 : {
+ _owner._sinTable.resize(4096);
+ _owner._cosTable.resize(4096);
+
+ for (uint i = 0; i < 4096; ++i) {
+ double f = (i * _owner._pi * 2) / 4096;
+ _owner._cosTable[i] = cos(f);
+ _owner._sinTable[i] = sin(f);
+ }
- _sinTable.resize(4096);
- _cosTable.resize(4096);
+ _owner._maxWorldDistance = sqrt(((128 * _owner._mapFullWidth) - 1) * ((128 * _owner._mapFullHeight) - 1) * 2);
+ _owner._depthBuffer.resize(g_nancy->_viewportData->bounds.width());
+ _owner._zBuffer.resize(g_nancy->_viewportData->bounds.width() * g_nancy->_viewportData->bounds.height(), 0);
+ _owner._lastZDepth = 0;
- for (uint i = 0; i < 4096; ++i) {
- double f = (i * _pi * 2) / 4096;
- _cosTable[i] = cos(f);
- _sinTable[i] = sin(f);
+ _loadState = kLoadTextures;
+ break;
}
+ case kLoadTextures: {
+ bool shouldBreak = false;
+
+ for (; _y < _owner._mapFullHeight; ++_y) {
+ if (_x >= _owner._mapFullWidth) {
+ _x = 0;
+ }
+
+ for (; _x < _owner._mapFullWidth && !shouldBreak; ++_x) {
+ uint32 wallMapVal = _owner._wallMap[_y * _owner._mapFullHeight + _x];
+
+ for (uint i = 0; i < 3; ++i) {
+ byte textureID = wallMapVal & 0xFF;
+
+ if (textureID & 0x80) {
+ if (!_owner._specialWallTextures.contains(textureID & 0x7F)) {
+ _owner.createTextureLightSourcing(&_owner._specialWallTextures[textureID & 0x7F], _owner._puzzleData->specialWallNames[(textureID & 0x7F) - 1]);
+ shouldBreak = true;
+ }
+ } else if (textureID) {
+ if (!_owner._wallTextures.contains(textureID & 0x7F)) {
+ _owner.createTextureLightSourcing(&_owner._wallTextures[textureID], _owner._puzzleData->wallNames[textureID - 1]);
+ shouldBreak = true;
+ }
+ }
+
+ wallMapVal >>= 8;
+ }
+
+ if (shouldBreak) {
+ break;
+ }
- _maxWorldDistance = sqrt(((128 * _mapFullWidth) - 1) * ((128 * _mapFullHeight) - 1) * 2);
- _depthBuffer.resize(viewport.width());
- _zBuffer.resize(viewport.width() * viewport.height());
+ int16 floorMapVal = _owner._floorMap[_y * _owner._mapFullWidth + _x];
+ int16 ceilingMapVal = _owner._ceilingMap[_y * _owner._mapFullWidth + _x];
+
+ if (!_owner._floorTextures.contains(floorMapVal)) {
+ _owner.createTextureLightSourcing(&_owner._floorTextures[floorMapVal], _owner._puzzleData->floorNames[floorMapVal]);
+ shouldBreak = true;
+ break;
+ }
- loadTextures();
- clearZBuffer();
+ if (!_owner._ceilingTextures.contains(ceilingMapVal)) {
+ _owner.createTextureLightSourcing(&_owner._ceilingTextures[ceilingMapVal], _owner._puzzleData->ceilingNames[ceilingMapVal]);
+ shouldBreak = true;
+ break;
+ }
+ }
- // TODO these need to be set according to the start position in _infoMap
- _playerRotation = 2048;
- _playerX = _playerY = 320;
+ if (shouldBreak) {
+ break;
+ }
+ }
+
+ if (!shouldBreak) {
+ for (auto &a : _owner._specialWallTextures) {
+ for (auto &tex : a._value) {
+ tex.setTransparentColor(g_nancy->_graphicsManager->getTransColor());
+ }
+ }
+
+ // TODO these need to be set according to the start position in _infoMap
+ _owner._playerRotation = 2048;
+ _owner._playerX = _owner._playerY = 320;
+
+ _isDone = true;
+ }
+
+ break;
+ }
+ }
+
+ return _isDone;
+}
+
+void RaycastPuzzle::init() {
+ _puzzleData = g_nancy->_raycastPuzzleData;
+ assert(_puzzleData);
+
+ RaycastDeferredLoader *loader = _loaderPtr.get();
+ if (!loader) {
+ _loaderPtr.reset(new RaycastDeferredLoader(*this, _mapWidth, _mapHeight, _wallHeight));
+ auto castedPtr = _loaderPtr.dynamicCast<DeferredLoader>();
+ g_nancy->addDeferredLoader(castedPtr);
+ } else {
+ if (loader->_isDone) {
+ _loaderPtr.reset();
+ registerGraphics();
+
+ _state = kRun;
+ }
+ }
}
void RaycastPuzzle::registerGraphics() {
@@ -955,10 +1080,6 @@ void RaycastPuzzle::execute() {
switch (_state) {
case kBegin:
init();
- registerGraphics();
-
- _state = kRun;
-
break;
case kRun:
// TODO check light switches
@@ -1237,44 +1358,7 @@ void RaycastPuzzle::drawMap() {
void RaycastPuzzle::loadTextures() {
// TODO this is slow and freezes the engine for a few seconds
- for (uint y = 0; y < _mapFullHeight; ++y) {
- for (uint x = 0; x < _mapFullWidth; ++x) {
- uint32 wallMapVal = _wallMap[y * _mapFullHeight + x];
-
- for (uint i = 0; i < 3; ++i) {
- byte textureID = wallMapVal & 0xFF;
-
- if (textureID & 0x80) {
- if (!_specialWallTextures.contains(textureID)) {
- createTextureLightSourcing(&_specialWallTextures[textureID & 0x7F], _puzzleData->specialWallNames[(textureID & 0x7F) - 1]);
- }
- } else if (textureID) {
- if (!_wallTextures.contains(textureID)) {
- createTextureLightSourcing(&_wallTextures[textureID], _puzzleData->wallNames[textureID - 1]);
- }
- }
-
- wallMapVal >>= 8;
- }
-
- int16 floorMapVal = _floorMap[y * _mapFullWidth + x];
- int16 ceilingMapVal = _ceilingMap[y * _mapFullWidth + x];
-
- if (!_floorTextures.contains(floorMapVal)) {
- createTextureLightSourcing(&_floorTextures[floorMapVal], _puzzleData->floorNames[floorMapVal]);
- }
-
- if (!_ceilingTextures.contains(ceilingMapVal)) {
- createTextureLightSourcing(&_ceilingTextures[ceilingMapVal], _puzzleData->ceilingNames[ceilingMapVal]);
- }
- }
- }
-
- for (auto &a : _specialWallTextures) {
- for (auto &tex : a._value) {
- tex.setTransparentColor(g_nancy->_graphicsManager->getTransColor());
- }
- }
+
}
void RaycastPuzzle::createTextureLightSourcing(Common::Array<Graphics::ManagedSurface> *array, Common::String &textureName) {
diff --git a/engines/nancy/action/raycastpuzzle.h b/engines/nancy/action/raycastpuzzle.h
index 1ed82d766ed..e843392eb0d 100644
--- a/engines/nancy/action/raycastpuzzle.h
+++ b/engines/nancy/action/raycastpuzzle.h
@@ -30,8 +30,13 @@ struct RCPR;
namespace Action {
+class RaycastDeferredLoader;
+class RaycastLevelBuilder;
+
// Action record implementing nancy3's maze minigame
class RaycastPuzzle : public RenderActionRecord {
+ friend class RaycastDeferredLoader;
+ friend class RaycastLevelBuilder;
public:
RaycastPuzzle() : RenderActionRecord(7), _map(7) {}
~RaycastPuzzle() override {}
@@ -44,6 +49,19 @@ public:
void handleInput(NancyInput &input) override;
void updateGraphics() override;
+protected:
+ Common::String getRecordTypeName() const override { return "RaycastPuzzle"; }
+ bool isViewportRelative() const override { return true; }
+
+ void loadTextures();
+ void createTextureLightSourcing(Common::Array<Graphics::ManagedSurface> *array, Common::String &textureName);
+
+ void drawMap();
+ void drawMaze();
+ void clearZBuffer();
+
+ void checkSwitch();
+
uint16 _mapWidth = 0;
uint16 _mapHeight = 0;
byte _wallHeight = 0;
@@ -100,19 +118,7 @@ public:
int32 _slowdownDeltaY = -1;
RCPR *_puzzleData = nullptr;
-
-protected:
- Common::String getRecordTypeName() const override { return "RaycastPuzzle"; }
- bool isViewportRelative() const override { return true; }
-
- void loadTextures();
- void createTextureLightSourcing(Common::Array<Graphics::ManagedSurface> *array, Common::String &textureName);
-
- void drawMap();
- void drawMaze();
- void clearZBuffer();
-
- void checkSwitch();
+ Common::SharedPtr<RaycastDeferredLoader> _loaderPtr;
};
} // End of namespace Action
diff --git a/engines/nancy/nancy.cpp b/engines/nancy/nancy.cpp
index 7e44027ebdc..333d45f11e3 100644
--- a/engines/nancy/nancy.cpp
+++ b/engines/nancy/nancy.cpp
@@ -239,6 +239,10 @@ void NancyEngine::setMouseEnabled(bool enabled) {
_cursorManager->showCursor(enabled); _input->setMouseInputEnabled(enabled);
}
+void NancyEngine::addDeferredLoader(Common::SharedPtr<DeferredLoader> &loaderPtr) {
+ _deferredLoaderObjects.push_back(Common::WeakPtr<DeferredLoader>(loaderPtr));
+}
+
Common::Error NancyEngine::run() {
setDebugger(new NancyConsole());
@@ -256,6 +260,8 @@ Common::Error NancyEngine::run() {
// Main loop
while (!shouldQuit()) {
+ uint32 frameEndTime = _system->getMillis() + 16;
+
_cursorManager->setCursorType(CursorManager::kNormalArrow);
_input->processEvents();
@@ -289,7 +295,41 @@ Common::Error NancyEngine::run() {
}
_system->updateScreen();
- _system->delayMillis(16);
+
+ // Use the spare time until the next frame to load larger data objects
+ // Some loading is guaranteed to happen even with no time left, to ensure
+ // slower systems won't be stuck waiting forever
+ if (_deferredLoaderObjects.size()) {
+ uint i = _deferredLoaderObjects.size() - 1;
+ int32 timePerObj = (frameEndTime - g_system->getMillis()) / _deferredLoaderObjects.size();
+
+ if (timePerObj < 0) {
+ timePerObj = 0;
+ }
+
+ for (auto *iter = _deferredLoaderObjects.begin(); iter < _deferredLoaderObjects.end(); ++iter) {
+ if (iter->expired()) {
+ iter = _deferredLoaderObjects.erase(iter);
+ } else {
+ auto objectPtr = iter->lock();
+ if (objectPtr) {
+ if (objectPtr->load(frameEndTime - (i * timePerObj))) {
+ iter = _deferredLoaderObjects.erase(iter);
+ }
+ --i;
+ }
+
+ if (_system->getMillis() > frameEndTime) {
+ break;
+ }
+ }
+ }
+ }
+
+ uint32 frameFinishTime = _system->getMillis();
+ if (frameFinishTime < frameEndTime) {
+ _system->delayMillis(frameEndTime - frameFinishTime);
+ }
}
if (State::Logo::hasInstance())
diff --git a/engines/nancy/nancy.h b/engines/nancy/nancy.h
index a26a7c64f62..62b1a25424b 100644
--- a/engines/nancy/nancy.h
+++ b/engines/nancy/nancy.h
@@ -24,6 +24,7 @@
#include "common/file.h"
#include "common/str.h"
+#include "common/ptr.h"
#include "engines/engine.h"
@@ -63,6 +64,7 @@ class SoundManager;
class GraphicsManager;
class CursorManager;
class NancyConsole;
+class DeferredLoader;
namespace State {
class State;
@@ -101,6 +103,8 @@ public:
void setMouseEnabled(bool enabled);
+ void addDeferredLoader(Common::SharedPtr<DeferredLoader> &loaderPtr);
+
// The first few games used 1/2 for false/true in
// inventory, logic conditions, and event flags
const byte _true;
@@ -164,6 +168,8 @@ private:
OSystem *_system;
const NancyGameDescription *_gameDescription;
+
+ Common::Array<Common::WeakPtr<DeferredLoader>> _deferredLoaderObjects;
};
extern NancyEngine *g_nancy;
diff --git a/engines/nancy/util.cpp b/engines/nancy/util.cpp
index 4a1fb16a388..39ec08ef8a8 100644
--- a/engines/nancy/util.cpp
+++ b/engines/nancy/util.cpp
@@ -20,6 +20,7 @@
#include "engines/nancy/nancy.h"
#include "engines/nancy/util.h"
+#include "common/system.h"
namespace Nancy {
@@ -145,4 +146,28 @@ void readFilenameArray(Common::Serializer &stream, Common::Array<Common::String>
}
}
+bool DeferredLoader::load(uint32 endTime) {
+ uint32 loopStartTime = g_system->getMillis();
+ uint32 loopTime = 0; // Stores the loop that took the longest time to complete
+
+ while (loopTime + loopStartTime < endTime) {
+ if (loadInner()) {
+ return true;
+ }
+
+ uint32 loopEndTime = g_system->getMillis();
+ loopTime = MAX<uint32>(loopEndTime - loopStartTime, loopTime);
+ loopStartTime = loopEndTime;
+
+ // We do this after at least one execution to avoid the case where the game runs below
+ // the target fps, and thus has no spare milliseconds until the next frame. This way
+ // we ensure loading actually gets done, at the expense of some lag
+ if (g_system->getMillis() < endTime) {
+ break;
+ }
+ }
+
+ return false;
+}
+
} // End of namespace Nancy
diff --git a/engines/nancy/util.h b/engines/nancy/util.h
index 8b226932150..a0eb19c5df9 100644
--- a/engines/nancy/util.h
+++ b/engines/nancy/util.h
@@ -36,6 +36,20 @@ void readFilename(Common::Serializer &stream, Common::String &inString, Common::
void readFilenameArray(Common::SeekableReadStream &stream, Common::Array<Common::String> &inArray, uint num);
void readFilenameArray(Common::Serializer &stream, Common::Array<Common::String> &inArray, uint num, Common::Serializer::Version minVersion = 0, Common::Serializer::Version maxVersion = Common::Serializer::kLastVersion);
+// Abstract base class used for loading data that would take too much time in a single frame
+class DeferredLoader {
+public:
+ DeferredLoader() {}
+ virtual ~DeferredLoader() {}
+
+ // Calls loadInner() one or many times, until its allotted time is done
+ bool load(uint32 endTime);
+
+protected:
+ // Contains the actual loading logic, split up into tasks that are as small as possible
+ virtual bool loadInner() = 0;
+};
+
} // End of namespace Nancy
#endif // NANCY_UTIL_H
Commit: 911a542b12a0f8adc3908ca4576e1418f4961812
https://github.com/scummvm/scummvm/commit/911a542b12a0f8adc3908ca4576e1418f4961812
Author: Kaloyan Chehlarski (strahy at outlook.com)
Date: 2023-08-23T12:15:06+03:00
Commit Message:
NANCY: Implement frame caching for AVF video
Added a frame cache to AVF videos, which allows for
preloading of frames using the new DeferredLoader class.
Changed paths:
engines/nancy/ui/viewport.cpp
engines/nancy/ui/viewport.h
engines/nancy/video.cpp
engines/nancy/video.h
diff --git a/engines/nancy/ui/viewport.cpp b/engines/nancy/ui/viewport.cpp
index 400ee338d4e..191125eeaa7 100644
--- a/engines/nancy/ui/viewport.cpp
+++ b/engines/nancy/ui/viewport.cpp
@@ -213,6 +213,7 @@ void Viewport::setFrame(uint frameNr) {
assert(frameNr < _decoder.getFrameCount());
const Graphics::Surface *newFrame = _decoder.decodeFrame(frameNr);
+ _decoder.seek(frameNr); // Seek to take advantage of caching
// Format 1 uses quarter-size images, while format 2 uses full-size ones
// Videos in TVD are always upside-down
diff --git a/engines/nancy/ui/viewport.h b/engines/nancy/ui/viewport.h
index 7ed0674179f..55683bee58a 100644
--- a/engines/nancy/ui/viewport.h
+++ b/engines/nancy/ui/viewport.h
@@ -51,7 +51,8 @@ public:
_currentFrame(0),
_videoFormat(kLargeVideoFormat),
_stickyCursorPos(-1, -1),
- _panningType(kPanNone) {}
+ _panningType(kPanNone),
+ _decoder(AVFDecoder::kLoadBidirectional) {}
virtual ~Viewport() { _decoder.close(); _fullFrame.free(); }
diff --git a/engines/nancy/video.cpp b/engines/nancy/video.cpp
index d51504639e1..ea212e2757c 100644
--- a/engines/nancy/video.cpp
+++ b/engines/nancy/video.cpp
@@ -26,9 +26,51 @@
#include "engines/nancy/video.h"
#include "engines/nancy/decompress.h"
#include "engines/nancy/graphics.h"
+#include "engines/nancy/util.h"
namespace Nancy {
+class VideoCacheLoader : public DeferredLoader {
+public:
+ VideoCacheLoader(AVFDecoder::AVFVideoTrack &owner) : _owner(owner) {}
+ virtual ~VideoCacheLoader() {}
+
+private:
+ bool loadInner() override;
+
+ AVFDecoder::AVFVideoTrack &_owner;
+};
+
+bool VideoCacheLoader::loadInner() {
+ AVFDecoder::CacheHint hint = _owner._cacheHint;
+ int frameID = _owner._curFrame;
+ int frameCount = _owner._frameCount;
+
+ for (int i = 0; i < frameCount; ++i) {
+ if (frameID < 0) {
+ frameID += frameCount;
+ }
+
+ if (frameID >= frameCount) {
+ frameID -= frameCount;
+ }
+
+ if (!_owner._frameCache[frameID].getPixels()) {
+ _owner.decodeFrame(frameID);
+ return false;
+ }
+
+ // Select next frame based on hint and play direction
+ if (hint != AVFDecoder::kLoadBidirectional) {
+ frameID = _owner._reversed ? frameID - 1 : frameID + 1;
+ } else {
+ frameID = _owner._curFrame + (i % 2 ? i >> 1 : -(i >> 1));
+ }
+ }
+
+ return true;
+}
+
AVFDecoder::~AVFDecoder() {
close();
}
@@ -63,7 +105,7 @@ bool AVFDecoder::loadStream(Common::SeekableReadStream *stream) {
stream->skip(1); // Unknown
}
- addTrack(new AVFVideoTrack(stream, chunkFileFormat));
+ addTrack(new AVFVideoTrack(stream, chunkFileFormat, _cacheHint));
return true;
}
@@ -85,7 +127,7 @@ bool AVFDecoder::atEnd() const {
return !track->isReversed() && track->endOfTrack() && track->getFrameTime(track->getFrameCount()) <= getTime();
}
-AVFDecoder::AVFVideoTrack::AVFVideoTrack(Common::SeekableReadStream *stream, uint32 chunkFileFormat) {
+AVFDecoder::AVFVideoTrack::AVFVideoTrack(Common::SeekableReadStream *stream, uint32 chunkFileFormat, CacheHint cacheHint) {
assert(stream);
_fileStream = stream;
_curFrame = -1;
@@ -111,9 +153,7 @@ AVFDecoder::AVFVideoTrack::AVFVideoTrack(Common::SeekableReadStream *stream, uin
if (comp != 1 && comp != 2)
error("Unknown compression type %d found in AVF", comp);
- _surface = new Graphics::Surface();
_pixelFormat = g_nancy->_graphicsManager->getInputPixelFormat();
- _surface->create(_width, _height, _pixelFormat);
_frameSize = _width * _height * _pixelFormat.bytesPerPixel;
_chunkInfo.reserve(_frameCount);
@@ -144,13 +184,21 @@ AVFDecoder::AVFVideoTrack::AVFVideoTrack(Common::SeekableReadStream *stream, uin
_chunkInfo.push_back(info);
}
+
+ _frameCache.resize(_frameCount);
+ _cacheHint = cacheHint;
+ _loaderPtr.reset(new VideoCacheLoader(*this));
+ auto castedPtr = _loaderPtr.dynamicCast<DeferredLoader>();
+ g_nancy->addDeferredLoader(castedPtr);
}
AVFDecoder::AVFVideoTrack::~AVFVideoTrack() {
delete _fileStream;
- _surface->free();
- delete _surface;
delete _dec;
+
+ for (Graphics::Surface &surf : _frameCache) {
+ surf.free();
+ }
}
bool AVFDecoder::AVFVideoTrack::seek(const Audio::Timestamp &time) {
@@ -219,6 +267,12 @@ bool AVFDecoder::AVFVideoTrack::decode(byte *outBuf, uint32 frameSize, Common::R
}
const Graphics::Surface *AVFDecoder::AVFVideoTrack::decodeFrame(uint frameNr) {
+ if (frameNr < _frameCache.size() && _frameCache[frameNr].getPixels()) {
+ // Frame is cached, return a pointer to it
+ _surface = &_frameCache[frameNr];
+ return _surface;
+ }
+
if (frameNr >= _chunkInfo.size()) {
warning("Frame %d doesn't exist", frameNr);
return nullptr;
@@ -241,6 +295,9 @@ const Graphics::Surface *AVFDecoder::AVFVideoTrack::decodeFrame(uint frameNr) {
return _surface;
}
+ Graphics::Surface &frameInCache = _frameCache[frameNr];
+ frameInCache.create(_width, _height, _pixelFormat);
+
byte *decompBuf = nullptr;
if (info.type == 0) {
// For type 0 we decompress straight to the surface, make sure we don't go out of bounds
@@ -249,7 +306,7 @@ const Graphics::Surface *AVFDecoder::AVFVideoTrack::decodeFrame(uint frameNr) {
return nullptr;
}
- decompBuf = (byte *)_surface->getPixels();
+ decompBuf = (byte *)frameInCache.getPixels();
} else {
// For types 1 and 2, we decompress to a temp buffer for decoding
decompBuf = new byte[info.size];
@@ -272,7 +329,7 @@ const Graphics::Surface *AVFDecoder::AVFVideoTrack::decodeFrame(uint frameNr) {
if (info.type != 0) {
Common::MemoryReadStream decompStr(decompBuf, info.size);
- decode((byte *)_surface->getPixels(), _frameSize, decompStr);
+ decode((byte *)frameInCache.getPixels(), _frameSize, decompStr);
}
if (info.type != 0) {
@@ -280,6 +337,7 @@ const Graphics::Surface *AVFDecoder::AVFVideoTrack::decodeFrame(uint frameNr) {
}
_refFrame = frameNr;
+ _surface = &frameInCache;
return _surface;
}
diff --git a/engines/nancy/video.h b/engines/nancy/video.h
index 9fa1f4e8047..b0e87cd52ed 100644
--- a/engines/nancy/video.h
+++ b/engines/nancy/video.h
@@ -36,9 +36,14 @@ struct Surface;
namespace Nancy {
class Decompressor;
+class VideoCacheLoader;
class AVFDecoder : public Video::VideoDecoder {
+ friend class VideoCacheLoader;
public:
+ enum CacheHint { kLoadForward, kLoadBidirectional };
+
+ AVFDecoder(CacheHint cacheHint = kLoadForward) : _cacheHint(cacheHint) {}
virtual ~AVFDecoder();
bool loadStream(Common::SeekableReadStream *stream) override;
@@ -46,11 +51,14 @@ public:
void addFrameTime(const uint16 timeToAdd);
bool atEnd() const;
+
private:
+ CacheHint _cacheHint;
class AVFVideoTrack : public FixedRateVideoTrack {
friend class AVFDecoder;
+ friend class VideoCacheLoader;
public:
- AVFVideoTrack(Common::SeekableReadStream *stream, uint32 chunkFileFormat);
+ AVFVideoTrack(Common::SeekableReadStream *stream, uint32 chunkFileFormat, CacheHint cacheHint);
virtual ~AVFVideoTrack();
uint16 getWidth() const override { return _width; }
@@ -93,6 +101,10 @@ private:
Decompressor *_dec;
bool _reversed;
bool _compressed;
+
+ Common::SharedPtr<VideoCacheLoader> _loaderPtr;
+ Common::Array<Graphics::Surface> _frameCache;
+ CacheHint _cacheHint;
};
};
Commit: 83b3d3a91bd2267e4601aee62e1d244f9a571f6d
https://github.com/scummvm/scummvm/commit/83b3d3a91bd2267e4601aee62e1d244f9a571f6d
Author: Kaloyan Chehlarski (strahy at outlook.com)
Date: 2023-08-23T12:15:06+03:00
Commit Message:
NANCY: Implement cel caching in ConversationCel
Added facilities for preloading and caching individual cels
in ConversationCel. This makes use of the new
DeferredLoader.
Changed paths:
engines/nancy/action/conversation.cpp
engines/nancy/action/conversation.h
diff --git a/engines/nancy/action/conversation.cpp b/engines/nancy/action/conversation.cpp
index abfe88cfd19..4db170e4ecd 100644
--- a/engines/nancy/action/conversation.cpp
+++ b/engines/nancy/action/conversation.cpp
@@ -554,11 +554,41 @@ Common::String ConversationVideo::getRecordTypeName() const {
}
}
+class ConversationCelLoader : public DeferredLoader {
+public:
+ ConversationCelLoader(ConversationCel &owner) : _owner(owner) {}
+
+private:
+ bool loadInner() override;
+
+ ConversationCel &_owner;
+};
+
+bool ConversationCelLoader::loadInner() {
+ for (uint i = _owner._curFrame; i < _owner._bodyCelNames.size(); ++i) {
+ if (!_owner._celCache.contains(_owner._bodyCelNames[i])) {
+ _owner.loadCel(_owner._bodyCelNames[i], _owner._bodyTreeName);
+ return false;
+ }
+
+ if (!_owner._celCache.contains(_owner._headCelNames[i])) {
+ _owner.loadCel(_owner._headCelNames[i], _owner._headTreeName);
+ return false;
+ }
+ }
+
+ return true;
+}
+
void ConversationCel::init() {
registerGraphics();
_curFrame = _firstFrame;
_nextFrameTime = g_nancy->getTotalPlayTime();
ConversationSound::init();
+
+ _loaderPtr.reset(new ConversationCelLoader(*this));
+ auto castedPtr = _loaderPtr.dynamicCast<DeferredLoader>();
+ g_nancy->addDeferredLoader(castedPtr);
}
void ConversationCel::registerGraphics() {
@@ -570,16 +600,14 @@ void ConversationCel::updateGraphics() {
uint32 currentTime = g_nancy->getTotalPlayTime();
if (_state == kRun && currentTime > _nextFrameTime && _curFrame <= _lastFrame) {
- Cel &curCel = _cels[_curFrame];
-
- g_nancy->_resource->loadImage(curCel.bodyCelName, curCel.bodySurf, _bodyTreeName, &curCel.bodySrc, &curCel.bodyDest);
- g_nancy->_resource->loadImage(curCel.headCelName, curCel.headSurf, _headTreeName, &curCel.headSrc, &curCel.headDest);
+ Cel &bodyCel = loadCel(_bodyCelNames[_curFrame], _bodyTreeName);
+ Cel &headCel = loadCel(_headCelNames[_curFrame], _headTreeName);
- _drawSurface.create(curCel.bodySurf, curCel.bodySrc);
- moveTo(curCel.bodyDest);
+ _drawSurface.create(bodyCel.surf, bodyCel.src);
+ moveTo(bodyCel.dest);
- _headRObj._drawSurface.create(curCel.headSurf, curCel.headSrc);
- _headRObj.moveTo(curCel.headDest);
+ _headRObj._drawSurface.create(headCel.surf, headCel.src);
+ _headRObj.moveTo(headCel.dest);
_nextFrameTime += _frameTime;
++_curFrame;
@@ -616,11 +644,11 @@ void ConversationCel::readData(Common::SeekableReadStream &stream) {
_frameTime = xsheet.readUint16LE();
xsheet.skip(2);
- _cels.resize(numFrames);
+ _bodyCelNames.resize(numFrames);
+ _headCelNames.resize(numFrames);
for (uint i = 0; i < numFrames; ++i) {
- Cel &cel = _cels[i];
- readFilename(xsheet, cel.bodyCelName);
- readFilename(xsheet, cel.headCelName);
+ readFilename(xsheet, _bodyCelNames[i]);
+ readFilename(xsheet, _headCelNames[i]);
// Zeroes
if (gameType >= kGameTypeNancy3) {
@@ -655,5 +683,16 @@ bool ConversationCel::isVideoDonePlaying() {
return _curFrame >= _lastFrame && _nextFrameTime <= g_nancy->getTotalPlayTime();
}
+ConversationCel::Cel &ConversationCel::loadCel(const Common::String &name, const Common::String &treeName) {
+ // Assumes head and body cels will be named differently
+ if (_celCache.contains(name)) {
+ return _celCache[name];
+ }
+
+ Cel &newCel = _celCache.getOrCreateVal(name);
+ g_nancy->_resource->loadImage(name, newCel.surf, treeName, &newCel.src, &newCel.dest);
+ return _celCache[name];
+}
+
} // End of namespace Action
} // End of namespace Nancy
diff --git a/engines/nancy/action/conversation.h b/engines/nancy/action/conversation.h
index 4e5595bde54..a2c0a0ac979 100644
--- a/engines/nancy/action/conversation.h
+++ b/engines/nancy/action/conversation.h
@@ -134,19 +134,28 @@ protected:
AVFDecoder _decoder;
};
+class ConversationCelLoader;
+
// Conversation with separate cels for the body and head of the character.
// Cels are separate images bundled inside a .cal file
class ConversationCel : public ConversationSound {
+ friend class ConversationCelLoader;
public:
+ ConversationCel() {}
+
+ void init() override;
+ void registerGraphics() override;
+ void updateGraphics() override;
+
+ void readData(Common::SeekableReadStream &stream) override;
+
+protected:
+ Common::String getRecordTypeName() const override { return "ConversationCel"; }
+
struct Cel {
- Common::String bodyCelName;
- Graphics::ManagedSurface bodySurf;
- Common::Rect bodySrc;
- Common::Rect bodyDest;
- Common::String headCelName;
- Graphics::ManagedSurface headSurf;
- Common::Rect headSrc;
- Common::Rect headDest;
+ Graphics::ManagedSurface surf;
+ Common::Rect src;
+ Common::Rect dest;
};
class HeadCel : public RenderObject {
@@ -155,17 +164,11 @@ public:
bool isViewportRelative() const override { return true; }
};
- ConversationCel() {}
-
- void init() override;
- void registerGraphics() override;
- void updateGraphics() override;
-
- void readData(Common::SeekableReadStream &stream) override;
-
bool isVideoDonePlaying() override;
+ Cel &loadCel(const Common::String &name, const Common::String &treeName);
- Common::Array<Cel> _cels;
+ Common::Array<Common::String> _bodyCelNames;
+ Common::Array<Common::String> _headCelNames;
Common::String _bodyTreeName;
Common::String _headTreeName;
@@ -180,8 +183,8 @@ public:
// We use the built-in RenderObject for the body
HeadCel _headRObj;
-protected:
- Common::String getRecordTypeName() const override { return "ConversationCel"; }
+ Common::HashMap<Common::String, Cel> _celCache;
+ Common::SharedPtr<ConversationCelLoader> _loaderPtr;
};
} // End of namespace Action
More information about the Scummvm-git-logs
mailing list