1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281
|
/*
* RowVisitor.cpp
* --------------
* Purpose: Class for recording which rows of a song has already been visited, used for detecting when a module starts to loop.
* Notes : The class keeps track of rows that have been visited by the player before.
* This way, we can tell when the module starts to loop, i.e. we can determine the song length,
* or find out that a given point of the module can never be reached.
*
* In some module formats, infinite loops can be achieved through pattern loops (e.g. E60 / E61 / E61 in one channel of a ProTracker MOD).
* To detect such loops, we store a set of loop counts across all channels encountered for each row.
* As soon as a set of loop counts is encountered twice for a specific row, we know that the track ends up in an infinite loop.
* As a result of this design, it is safe to evaluate pattern loops in CSoundFile::GetLength.
* Authors: OpenMPT Devs
* The OpenMPT source code is released under the BSD license. Read LICENSE for more details.
*/
#include "stdafx.h"
#include "RowVisitor.h"
#include "Sndfile.h"
OPENMPT_NAMESPACE_BEGIN
RowVisitor::LoopState::LoopState(const ChannelStates &chnState, const bool ignoreRow)
{
// Rather than storing the exact loop count vector, we compute an FNV-1a 64-bit hash of it.
// This means we can store the loop state in a small and fixed amount of memory.
// In theory there is the possibility of hash collisions for different loop states, but in practice,
// the relevant inputs for the hashing algorithm are extremely unlikely to produce collisions.
// There may be better hashing algorithms, but many of them are much more complex and cannot be applied easily in an incremental way.
uint64 hash = FNV1a_BASIS;
if(ignoreRow)
{
hash = (hash ^ 0xFFu) * FNV1a_PRIME;
#ifdef MPT_VERIFY_ROWVISITOR_LOOPSTATE
m_counts.emplace_back(uint8(0xFF), uint8(0xFF));
#endif
}
for(size_t chn = 0; chn < chnState.size(); chn++)
{
if(chnState[chn].nPatternLoopCount)
{
static_assert(MAX_BASECHANNELS <= 256, "Channel index cannot be used as byte input for hash generator");
static_assert(sizeof(chnState[0].nPatternLoopCount) <= sizeof(uint8), "Loop count cannot be used as byte input for hash generator");
hash = (hash ^ chn) * FNV1a_PRIME;
hash = (hash ^ chnState[chn].nPatternLoopCount) * FNV1a_PRIME;
#ifdef MPT_VERIFY_ROWVISITOR_LOOPSTATE
m_counts.emplace_back(static_cast<uint8>(chn), chnState[chn].nPatternLoopCount);
#endif
}
}
m_hash = hash;
}
RowVisitor::RowVisitor(const CSoundFile &sndFile, SEQUENCEINDEX sequence)
: m_sndFile(sndFile)
, m_sequence(sequence)
{
Initialize(true);
}
void RowVisitor::MoveVisitedRowsFrom(RowVisitor &other) noexcept
{
m_visitedRows = std::move(other.m_visitedRows);
m_visitedLoopStates = std::move(other.m_visitedLoopStates);
}
const ModSequence &RowVisitor::Order() const
{
if(m_sequence >= m_sndFile.Order.GetNumSequences())
return m_sndFile.Order();
else
return m_sndFile.Order(m_sequence);
}
// Resize / clear the row vector.
// If reset is true, the vector is not only resized to the required dimensions, but also completely cleared (i.e. all visited rows are reset).
void RowVisitor::Initialize(bool reset)
{
auto &order = Order();
const ORDERINDEX endOrder = order.GetLengthTailTrimmed();
bool reserveLoopStates = true;
m_visitedRows.resize(endOrder);
if(reset)
{
reserveLoopStates = m_visitedLoopStates.empty();
for(auto &loopState : m_visitedLoopStates)
{
loopState.second.clear();
}
m_rowsSpentInLoops = 0;
}
std::vector<uint8> loopCount;
std::vector<ORDERINDEX> visitedPatterns(m_sndFile.Patterns.GetNumPatterns(), ORDERINDEX_INVALID);
for(ORDERINDEX ord = 0; ord < endOrder; ord++)
{
const PATTERNINDEX pat = order[ord];
const ROWINDEX numRows = VisitedRowsVectorSize(pat);
auto &visitedRows = m_visitedRows[ord];
if(reset)
visitedRows.assign(numRows, false);
else
visitedRows.resize(numRows, false);
if(!reserveLoopStates || !order.IsValidPat(ord))
continue;
const ROWINDEX startRow = std::min(static_cast<ROWINDEX>(reset ? 0 : visitedRows.size()), numRows);
auto insertionHint = m_visitedLoopStates.end();
if(visitedPatterns[pat] != ORDERINDEX_INVALID)
{
// We visited this pattern before, copy over the results
const auto begin = m_visitedLoopStates.lower_bound({visitedPatterns[pat], startRow});
const auto end = (begin != m_visitedLoopStates.end()) ? m_visitedLoopStates.lower_bound({visitedPatterns[pat], numRows}) : m_visitedLoopStates.end();
for(auto pos = begin; pos != end; ++pos)
{
LoopStateSet loopStates;
loopStates.reserve(pos->second.capacity());
insertionHint = ++m_visitedLoopStates.insert_or_assign(insertionHint, {ord, pos->first.second}, std::move(loopStates));
}
continue;
}
// Pre-allocate loop count state
const auto &pattern = m_sndFile.Patterns[pat];
loopCount.assign(pattern.GetNumChannels(), 0);
for(ROWINDEX i = numRows; i != startRow; i--)
{
const ROWINDEX row = i - 1;
uint32 maxLoopStates = 1;
auto m = pattern.GetpModCommand(row, 0);
// Break condition: If it's more than 16, it's probably wrong :) exact loop count depends on how loops overlap.
for(CHANNELINDEX chn = 0; chn < pattern.GetNumChannels() && maxLoopStates < 16; chn++, m++)
{
auto count = loopCount[chn];
if((m->command == CMD_S3MCMDEX && (m->param & 0xF0) == 0xB0) || (m->command == CMD_MODCMDEX && (m->param & 0xF0) == 0x60))
{
loopCount[chn] = (m->param & 0x0F);
if(loopCount[chn])
count = loopCount[chn];
}
if(count)
maxLoopStates *= (count + 1);
}
if(maxLoopStates > 1)
{
LoopStateSet loopStates;
loopStates.reserve(maxLoopStates);
insertionHint = m_visitedLoopStates.insert_or_assign(insertionHint, {ord, row}, std::move(loopStates));
}
}
// Only use this order as a blueprint for other orders using the same pattern if we fully parsed the pattern.
if(startRow == 0)
visitedPatterns[pat] = ord;
}
}
// Mark an order/row combination as visited and returns true if it was visited before.
bool RowVisitor::Visit(ORDERINDEX ord, ROWINDEX row, const ChannelStates &chnState, bool ignoreRow)
{
auto &order = Order();
if(ord >= order.size() || row >= VisitedRowsVectorSize(order[ord]))
return false;
// The module might have been edited in the meantime - so we have to extend this a bit.
if(ord >= m_visitedRows.size() || row >= m_visitedRows[ord].size())
{
Initialize(false);
// If it's still past the end of the vector, this means that ord >= order.GetLengthTailTrimmed(), i.e. we are trying to play an empty order.
if(ord >= m_visitedRows.size())
return false;
}
MPT_ASSERT(chnState.size() >= m_sndFile.GetNumChannels());
LoopState newState{chnState.first(m_sndFile.GetNumChannels()), ignoreRow};
const auto rowLoopState = m_visitedLoopStates.find({ord, row});
const bool oldHadLoops = (rowLoopState != m_visitedLoopStates.end() && !rowLoopState->second.empty());
const bool newHasLoops = newState.HasLoops();
const bool wasVisited = m_visitedRows[ord][row];
// Check if new state is part of row state already. If so, we visited this row already and thus the module must be looping
if(!oldHadLoops && !newHasLoops && wasVisited)
return true;
if(oldHadLoops && mpt::contains(rowLoopState->second, newState))
return true;
if(newHasLoops)
m_rowsSpentInLoops++;
if(oldHadLoops || newHasLoops)
{
// Convert to set representation if it isn't already
if(!oldHadLoops && wasVisited)
m_visitedLoopStates[{ord, row}].emplace_back();
m_visitedLoopStates[{ord, row}].emplace_back(std::move(newState));
}
m_visitedRows[ord][row] = true;
return false;
}
// Get the needed vector size for a given pattern.
ROWINDEX RowVisitor::VisitedRowsVectorSize(PATTERNINDEX pattern) const noexcept
{
if(m_sndFile.Patterns.IsValidPat(pattern))
return m_sndFile.Patterns[pattern].GetNumRows();
else
return 1; // Non-existing patterns consist of a "fake" row.
}
// Find the first row that has not been played yet.
// The order and row is stored in the order and row variables on success, on failure they contain invalid values.
// If onlyUnplayedPatterns is true (default), only completely unplayed patterns are considered, otherwise a song can start on any unplayed row.
// Function returns true on success.
bool RowVisitor::GetFirstUnvisitedRow(ORDERINDEX &ord, ROWINDEX &row, bool onlyUnplayedPatterns) const
{
const auto &order = Order();
const ORDERINDEX endOrder = order.GetLengthTailTrimmed();
for(ORDERINDEX o = 0; o < endOrder; o++)
{
if(!order.IsValidPat(o))
continue;
if(o >= m_visitedRows.size())
{
// Not yet initialized => unvisited
ord = o;
row = 0;
return true;
}
const auto &visitedRows = m_visitedRows[o];
ROWINDEX firstUnplayedRow = 0;
for(; firstUnplayedRow < visitedRows.size(); firstUnplayedRow++)
{
if(visitedRows[firstUnplayedRow] == onlyUnplayedPatterns)
break;
}
if(onlyUnplayedPatterns && firstUnplayedRow == visitedRows.size())
{
// No row of this pattern has been played yet.
ord = o;
row = 0;
return true;
} else if(!onlyUnplayedPatterns)
{
// Return the first unplayed row in this pattern
if(firstUnplayedRow < visitedRows.size())
{
ord = o;
row = firstUnplayedRow;
return true;
}
if(visitedRows.size() < m_sndFile.Patterns[order[o]].GetNumRows())
{
// History is not fully initialized
ord = o;
row = static_cast<ROWINDEX>(visitedRows.size());
return true;
}
}
}
// Didn't find anything :(
ord = ORDERINDEX_INVALID;
row = ROWINDEX_INVALID;
return false;
}
OPENMPT_NAMESPACE_END
|