mirror of https://github.com/mosra/magnum.git
Browse Source
Replaces the old & extremely useless Profiler. Doesn't have everything I want yet (missing stddev and fancier GPU queries), that'll come later.pull/432/merge
12 changed files with 2905 additions and 3 deletions
@ -0,0 +1,112 @@
|
||||
/*
|
||||
This file is part of Magnum. |
||||
|
||||
Copyright © 2010, 2011, 2012, 2013, 2014, 2015, 2016, 2017, 2018, 2019 |
||||
Vladimír Vondruš <mosra@centrum.cz> |
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a |
||||
copy of this software and associated documentation files (the "Software"), |
||||
to deal in the Software without restriction, including without limitation |
||||
the rights to use, copy, modify, merge, publish, distribute, sublicense, |
||||
and/or sell copies of the Software, and to permit persons to whom the |
||||
Software is furnished to do so, subject to the following conditions: |
||||
|
||||
The above copyright notice and this permission notice shall be included |
||||
in all copies or substantial portions of the Software. |
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR |
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, |
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL |
||||
THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER |
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING |
||||
FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER |
||||
DEALINGS IN THE SOFTWARE. |
||||
*/ |
||||
|
||||
#include <sstream> |
||||
#include <Corrade/Utility/DebugStl.h> |
||||
|
||||
#include "Magnum/DebugTools/FrameProfiler.h" |
||||
|
||||
/* Hacking around the fugly windowlessapp setup by including OpenGLTester */ |
||||
#include "Magnum/GL/OpenGLTester.h" |
||||
|
||||
using namespace Magnum; |
||||
|
||||
class FrameProfiler: public Platform::WindowlessApplication { |
||||
public: |
||||
explicit FrameProfiler(const Arguments& arguments); |
||||
|
||||
int exec() override { return 0; } |
||||
}; |
||||
|
||||
FrameProfiler::FrameProfiler(const Arguments& arguments): Platform::WindowlessApplication{arguments} { |
||||
/* Enable everything in the GL profiler and then introspect it to fake
|
||||
its output 1:1 */ |
||||
DebugTools::GLFrameProfiler glProfiler{ |
||||
DebugTools::GLFrameProfiler::Value::FrameTime| |
||||
DebugTools::GLFrameProfiler::Value::CpuDuration| |
||||
DebugTools::GLFrameProfiler::Value::GpuDuration| |
||||
DebugTools::GLFrameProfiler::Value::VertexFetchRatio| |
||||
DebugTools::GLFrameProfiler::Value::PrimitiveClipRatio |
||||
, 50}; |
||||
|
||||
DebugTools::FrameProfiler profiler{{ |
||||
DebugTools::FrameProfiler::Measurement{ |
||||
glProfiler.measurementName(0), |
||||
glProfiler.measurementUnits(0), |
||||
glProfiler.measurementDelay(2), |
||||
[](void*, UnsignedInt) {}, |
||||
[](void*, UnsignedInt) {}, |
||||
[](void*, UnsignedInt, UnsignedInt) { |
||||
return UnsignedLong{16651567}; |
||||
}, nullptr}, |
||||
DebugTools::FrameProfiler::Measurement{ |
||||
glProfiler.measurementName(1), |
||||
glProfiler.measurementUnits(1), |
||||
glProfiler.measurementDelay(2), |
||||
[](void*, UnsignedInt) {}, |
||||
[](void*, UnsignedInt) {}, |
||||
[](void*, UnsignedInt, UnsignedInt) { |
||||
return UnsignedLong{14720000}; |
||||
}, nullptr}, |
||||
DebugTools::FrameProfiler::Measurement{ |
||||
glProfiler.measurementName(2), |
||||
glProfiler.measurementUnits(2), |
||||
glProfiler.measurementDelay(2), |
||||
[](void*, UnsignedInt) {}, |
||||
[](void*, UnsignedInt) {}, |
||||
[](void*, UnsignedInt, UnsignedInt) { |
||||
return UnsignedLong{10890000}; |
||||
}, nullptr}, |
||||
DebugTools::FrameProfiler::Measurement{ |
||||
glProfiler.measurementName(3), |
||||
glProfiler.measurementUnits(3), |
||||
glProfiler.measurementDelay(3), |
||||
[](void*, UnsignedInt) {}, |
||||
[](void*, UnsignedInt) {}, |
||||
[](void*, UnsignedInt, UnsignedInt) { |
||||
return UnsignedLong{240}; |
||||
}, nullptr}, |
||||
DebugTools::FrameProfiler::Measurement{ |
||||
glProfiler.measurementName(4), |
||||
glProfiler.measurementUnits(4), |
||||
glProfiler.measurementDelay(4), |
||||
[](void*, UnsignedInt) {}, |
||||
[](void*, UnsignedInt) {}, |
||||
[](void*, UnsignedInt, UnsignedInt) { |
||||
return UnsignedLong{59670}; |
||||
}, nullptr}, |
||||
}, 50}; |
||||
|
||||
for(std::size_t i = 0; i != 100; ++i) { |
||||
profiler.beginFrame(); |
||||
profiler.endFrame(); |
||||
} |
||||
|
||||
std::ostringstream out; /* we don't want a TTY */ |
||||
profiler.printStatistics(Debug{&out}, 1); |
||||
Debug{Debug::Flag::NoNewlineAtTheEnd} << out.str(); |
||||
} |
||||
|
||||
MAGNUM_WINDOWLESSAPPLICATION_MAIN(FrameProfiler) |
||||
@ -0,0 +1,665 @@
|
||||
/*
|
||||
This file is part of Magnum. |
||||
|
||||
Copyright © 2010, 2011, 2012, 2013, 2014, 2015, 2016, 2017, 2018, 2019 |
||||
Vladimír Vondruš <mosra@centrum.cz> |
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a |
||||
copy of this software and associated documentation files (the "Software"), |
||||
to deal in the Software without restriction, including without limitation |
||||
the rights to use, copy, modify, merge, publish, distribute, sublicense, |
||||
and/or sell copies of the Software, and to permit persons to whom the |
||||
Software is furnished to do so, subject to the following conditions: |
||||
|
||||
The above copyright notice and this permission notice shall be included |
||||
in all copies or substantial portions of the Software. |
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR |
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, |
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL |
||||
THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER |
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING |
||||
FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER |
||||
DEALINGS IN THE SOFTWARE. |
||||
*/ |
||||
|
||||
#include "FrameProfiler.h" |
||||
|
||||
#include <chrono> |
||||
#include <sstream> |
||||
#include <Corrade/Containers/EnumSet.hpp> |
||||
#include <Corrade/Containers/GrowableArray.h> |
||||
#include <Corrade/Utility/DebugStl.h> |
||||
#include <Corrade/Utility/FormatStl.h> |
||||
|
||||
#include "Magnum/Math/Functions.h" |
||||
#ifdef MAGNUM_TARGET_GL |
||||
#include "Magnum/GL/TimeQuery.h" |
||||
#ifndef MAGNUM_TARGET_GLES |
||||
#include "Magnum/GL/PipelineStatisticsQuery.h" |
||||
#endif |
||||
#endif |
||||
|
||||
namespace Magnum { namespace DebugTools { |
||||
|
||||
FrameProfiler::Measurement::Measurement(const std::string& name, const Units units, void(*const begin)(void*), UnsignedLong(*const end)(void*), void* const state): _name{name}, _end{nullptr}, _state{state}, _units{units}, _delay{0} { |
||||
_begin.immediate = begin; |
||||
_query.immediate = end; |
||||
} |
||||
|
||||
FrameProfiler::Measurement::Measurement(const std::string& name, const Units units, const UnsignedInt delay, void(*const begin)(void*, UnsignedInt), void(*const end)(void*, UnsignedInt), UnsignedLong(*const query)(void*, UnsignedInt, UnsignedInt), void* const state): _name{name}, _state{state}, _units{units}, _delay{delay} { |
||||
CORRADE_ASSERT(delay >= 1, "DebugTools::FrameProfiler::Measurement: delay can't be zero", ); |
||||
_begin.delayed = begin; |
||||
_end = end; |
||||
_query.delayed = query; |
||||
} |
||||
|
||||
FrameProfiler::FrameProfiler() noexcept = default; |
||||
|
||||
FrameProfiler::FrameProfiler(Containers::Array<Measurement>&& measurements, std::size_t maxFrameCount) noexcept { |
||||
setup(std::move(measurements), maxFrameCount); |
||||
} |
||||
|
||||
FrameProfiler::FrameProfiler(const std::initializer_list<Measurement> measurements, const std::size_t maxFrameCount): FrameProfiler{Containers::array(measurements), maxFrameCount} {} |
||||
|
||||
FrameProfiler::FrameProfiler(FrameProfiler&& other) noexcept: |
||||
_enabled{other._enabled}, |
||||
#ifndef CORRADE_NO_ASSERT |
||||
_beginFrameCalled{other._beginFrameCalled}, |
||||
#endif |
||||
_currentData{other._currentData}, |
||||
_maxFrameCount{other._maxFrameCount}, |
||||
_measuredFrameCount{other._measuredFrameCount}, |
||||
_measurements{std::move(other._measurements)}, |
||||
_data{std::move(other._data)} |
||||
{ |
||||
/* For all state pointers that point to &other patch them to point to this
|
||||
instead, to account for 90% of use cases of derived classes */ |
||||
for(Measurement& measurement: _measurements) |
||||
if(measurement._state == &other) measurement._state = this; |
||||
} |
||||
|
||||
FrameProfiler& FrameProfiler::operator=(FrameProfiler&& other) noexcept { |
||||
using std::swap; |
||||
swap(_enabled, other._enabled); |
||||
#ifndef CORRADE_NO_ASSERT |
||||
swap(_beginFrameCalled, other._beginFrameCalled); |
||||
#endif |
||||
swap(_currentData, other._currentData); |
||||
swap(_maxFrameCount, other._maxFrameCount); |
||||
swap(_measuredFrameCount, other._measuredFrameCount); |
||||
swap(_measurements, other._measurements); |
||||
swap(_data, other._data); |
||||
|
||||
/* For all state pointers that point to &other patch them to point to this
|
||||
instead, to account for 90% of use cases of derived classes */ |
||||
for(Measurement& measurement: _measurements) |
||||
if(measurement._state == &other) measurement._state = this; |
||||
|
||||
/* And the same the other way to avoid the other instance accidentally
|
||||
affecting out measurements */ |
||||
for(Measurement& measurement: other._measurements) |
||||
if(measurement._state == this) measurement._state = &other; |
||||
|
||||
return *this; |
||||
} |
||||
|
||||
void FrameProfiler::setup(Containers::Array<Measurement>&& measurements, const std::size_t maxFrameCount) { |
||||
CORRADE_ASSERT(maxFrameCount >= 1, "DebugTools::FrameProfiler::setup(): max frame count can't be zero", ); |
||||
|
||||
_maxFrameCount = maxFrameCount; |
||||
_measurements = std::move(measurements); |
||||
arrayReserve(_data, maxFrameCount*_measurements.size()); |
||||
|
||||
/* Calculate the max delay, which signalizes when data will be available.
|
||||
Non-delayed measurements are distinguished by _delay set to 0, so start |
||||
with 1 to exclude these. */ |
||||
for(const Measurement& measurement: _measurements) { |
||||
/* Max frame count is always >= 1, so even if _delay is 0 the condition
|
||||
makes sense and we don't need to do a max() */ |
||||
CORRADE_ASSERT(maxFrameCount >= measurement._delay, |
||||
"DebugTools::FrameProfiler::setup(): max delay" << measurement._delay << "is larger than max frame count" << maxFrameCount, ); |
||||
} |
||||
|
||||
/* Reset to have a clean slate in case we did some other measurements
|
||||
before */ |
||||
enable(); |
||||
} |
||||
|
||||
void FrameProfiler::setup(const std::initializer_list<Measurement> measurements, const std::size_t maxFrameCount) { |
||||
setup(Containers::array(measurements), maxFrameCount); |
||||
} |
||||
|
||||
void FrameProfiler::enable() { |
||||
_enabled = true; |
||||
#ifndef CORRADE_NO_ASSERT |
||||
_beginFrameCalled = false; |
||||
#endif |
||||
_currentData = 0; |
||||
_measuredFrameCount = 0; |
||||
arrayResize(_data, 0); |
||||
|
||||
/* Wipe out no longer relevant moving sums from all measurements, and
|
||||
delayed measurement indices as well (tho for these it's not so |
||||
important) */ |
||||
for(Measurement& measurement: _measurements) { |
||||
measurement._movingSum = 0; |
||||
measurement._current = 0; |
||||
} |
||||
} |
||||
|
||||
void FrameProfiler::disable() { |
||||
_enabled = false; |
||||
} |
||||
|
||||
void FrameProfiler::beginFrame() { |
||||
if(!_enabled) return; |
||||
|
||||
CORRADE_ASSERT(!_beginFrameCalled, "DebugTools::FrameProfiler::beginFrame(): expected end of frame", ); |
||||
#ifndef CORRADE_NO_ASSERT |
||||
_beginFrameCalled = true; |
||||
#endif |
||||
|
||||
/* For all measurements call the begin function */ |
||||
for(const Measurement& measurement: _measurements) { |
||||
if(!measurement._delay) |
||||
measurement._begin.immediate(measurement._state); |
||||
else |
||||
measurement._begin.delayed(measurement._state, measurement._current); |
||||
} |
||||
} |
||||
|
||||
/* For delay = 1 returns _currentData */ |
||||
std::size_t FrameProfiler::delayedCurrentData(UnsignedInt delay) const { |
||||
CORRADE_INTERNAL_ASSERT(delay >= 1); |
||||
|
||||
/* The delayed frame is current or before current */ |
||||
if(_currentData >= delay - 1) |
||||
return _currentData - delay + 1; |
||||
|
||||
/* If we have all data, wrap around. If we don't have all data yet, such
|
||||
value doesn't exist and thus this will return an OOB index. If |
||||
everything is implemented correctly, it won't be accessed in any way. */ |
||||
return _maxFrameCount + _currentData - delay + 1; |
||||
} |
||||
|
||||
void FrameProfiler::endFrame() { |
||||
if(!_enabled) return; |
||||
|
||||
CORRADE_ASSERT(_beginFrameCalled, "DebugTools::FrameProfiler::endFrame(): expected begin of frame", ); |
||||
#ifndef CORRADE_NO_ASSERT |
||||
_beginFrameCalled = false; |
||||
#endif |
||||
|
||||
/* If we don't have all frames yet, enlarge the array */ |
||||
if(++_measuredFrameCount <= _maxFrameCount) { |
||||
CORRADE_INTERNAL_ASSERT(_measurements.empty() || _currentData == _data.size()/_measurements.size()); |
||||
arrayAppend(_data, Containers::NoInit, _measurements.size()); |
||||
} |
||||
|
||||
/* Wrap up measurements for this frame */ |
||||
for(std::size_t i = 0; i != _measurements.size(); ++i) { |
||||
Measurement& measurement = _measurements[i]; |
||||
const UnsignedInt measurementDelay = Math::max(1u, measurement._delay); |
||||
|
||||
/* Where to save currently queried data. For _delay of 0 or 1,
|
||||
delayedCurrentData(Math::max(1u, measurement._delay)) is equal to |
||||
_currentData. */ |
||||
UnsignedLong& currentMeasurementData = _data[delayedCurrentData(measurementDelay)*_measurements.size() + i]; |
||||
|
||||
/* If we're wrapping around, subtract the oldest data from the moving
|
||||
average so we can reuse the memory for currently queried data */ |
||||
if(_measuredFrameCount > _maxFrameCount + measurementDelay - 1) { |
||||
CORRADE_INTERNAL_ASSERT(measurement._movingSum >= currentMeasurementData); |
||||
measurement._movingSum -= currentMeasurementData; |
||||
} |
||||
|
||||
/* Simply save the data if not delayed */ |
||||
if(!measurement._delay) |
||||
currentMeasurementData = measurement._query.immediate(measurement._state); |
||||
|
||||
/* For delayed measurements call the end function for current frame and
|
||||
then save the data for the delayed frame */ |
||||
else { |
||||
measurement._end(measurement._state, measurement._current); |
||||
|
||||
/* The slot from which we just retrieved a delayed value will be
|
||||
reused for a a new value next frame */ |
||||
const UnsignedInt previous = (measurement._current + 1) % measurement._delay; |
||||
if(_measuredFrameCount >= measurement._delay) { |
||||
currentMeasurementData = |
||||
measurement._query.delayed(measurement._state, previous, measurement._current); |
||||
} |
||||
measurement._current = previous; |
||||
} |
||||
} |
||||
|
||||
/* Process the new data if we have enough frames even for the largest
|
||||
delay */ |
||||
for(std::size_t i = 0; i != _measurements.size(); ++i) { |
||||
Measurement& measurement = _measurements[i]; |
||||
const UnsignedInt measurementDelay = Math::max(1u, measurement._delay); |
||||
|
||||
/* If we have enough frames, add the new measurement to the moving sum.
|
||||
For _delay of 0 or 1, delayedCurrentData(Math::max(1u, measurement._delay)) |
||||
is equal to _currentData. */ |
||||
if(_measuredFrameCount >= measurementDelay) |
||||
_measurements[i]._movingSum += _data[delayedCurrentData(measurementDelay)*_measurements.size() + i]; |
||||
} |
||||
|
||||
/* Advance & wraparound the index where data will be saved for the next
|
||||
frame */ |
||||
_currentData = (_currentData + 1) % _maxFrameCount; |
||||
} |
||||
|
||||
std::string FrameProfiler::measurementName(const std::size_t id) const { |
||||
CORRADE_ASSERT(id < _measurements.size(), |
||||
"DebugTools::FrameProfiler::measurementName(): index" << id << "out of range for" << _measurements.size() << "measurements", {}); |
||||
return _measurements[id]._name; |
||||
} |
||||
|
||||
FrameProfiler::Units FrameProfiler::measurementUnits(const std::size_t id) const { |
||||
CORRADE_ASSERT(id < _measurements.size(), |
||||
"DebugTools::FrameProfiler::measurementUnits(): index" << id << "out of range for" << _measurements.size() << "measurements", {}); |
||||
return _measurements[id]._units; |
||||
} |
||||
|
||||
UnsignedInt FrameProfiler::measurementDelay(const std::size_t id) const { |
||||
CORRADE_ASSERT(id < _measurements.size(), |
||||
"DebugTools::FrameProfiler::measurementDelay(): index" << id << "out of range for" << _measurements.size() << "measurements", {}); |
||||
return Math::max(_measurements[id]._delay, 1u); |
||||
} |
||||
|
||||
bool FrameProfiler::isMeasurementAvailable(const std::size_t id) const { |
||||
CORRADE_ASSERT(id < _measurements.size(), |
||||
"DebugTools::FrameProfiler::measurementDelay(): index" << id << "out of range for" << _measurements.size() << "measurements", {}); |
||||
return _measuredFrameCount >= Math::max(_measurements[id]._delay, 1u); |
||||
} |
||||
|
||||
Double FrameProfiler::measurementDataInternal(const Measurement& measurement) const { |
||||
return Double(measurement._movingSum)/ |
||||
Math::min(_measuredFrameCount - Math::max(measurement._delay, 1u) + 1, _maxFrameCount); |
||||
} |
||||
|
||||
Double FrameProfiler::measurementMean(const std::size_t id) const { |
||||
CORRADE_ASSERT(id < _measurements.size(), |
||||
"DebugTools::FrameProfiler::measurementMean(): index" << id << "out of range for" << _measurements.size() << "measurements", {}); |
||||
CORRADE_ASSERT(_measuredFrameCount >= Math::max(_measurements[id]._delay, 1u), "DebugTools::FrameProfiler::measurementMean(): measurement data available after" << Math::max(_measurements[id]._delay, 1u) - _measuredFrameCount << "more frames", {}); |
||||
|
||||
return measurementDataInternal(_measurements[id]); |
||||
} |
||||
|
||||
namespace { |
||||
|
||||
/* Based on Corrade/TestSuite/Implementation/BenchmarkStats.h */ |
||||
|
||||
void printValue(Utility::Debug& out, const Double mean, const Double divisor, const char* const unitPrefix, const char* const units) { |
||||
out << Debug::boldColor(Debug::Color::Green) |
||||
<< Utility::formatString("{:.2f}", mean/divisor) << Debug::resetColor |
||||
<< Debug::nospace << unitPrefix << Debug::nospace << units; |
||||
} |
||||
|
||||
void printTime(Utility::Debug& out, const Double mean) { |
||||
if(mean >= 1000000000.0) |
||||
printValue(out, mean, 1000000000.0, " ", "s"); |
||||
else if(mean >= 1000000.0) |
||||
printValue(out, mean, 1000000.0, " m", "s"); |
||||
else if(mean >= 1000.0) |
||||
printValue(out, mean, 1000.0, " µ", "s"); |
||||
else |
||||
printValue(out, mean, 1.0, " n", "s"); |
||||
} |
||||
|
||||
void printCount(Utility::Debug& out, const Double mean, Double multiplier, const char* const units) { |
||||
if(mean >= multiplier*multiplier*multiplier) |
||||
printValue(out, mean, multiplier*multiplier*multiplier, " G", units); |
||||
else if(mean >= multiplier*multiplier) |
||||
printValue(out, mean, multiplier*multiplier, " M", units); |
||||
else if(mean >= multiplier) |
||||
printValue(out, mean, multiplier, " k", units); |
||||
else |
||||
printValue(out, mean, 1.0, std::strlen(units) ? " " : "", units); |
||||
} |
||||
|
||||
} |
||||
|
||||
void FrameProfiler::printStatisticsInternal(Debug& out) const { |
||||
out << Debug::boldColor(Debug::Color::Default) << "Last" |
||||
<< Debug::boldColor(Debug::Color::Cyan) |
||||
<< Math::min(_measuredFrameCount, _maxFrameCount) |
||||
<< Debug::boldColor(Debug::Color::Default) << "frames:"; |
||||
|
||||
for(const Measurement& measurement: _measurements) { |
||||
out << Debug::newline << " " << Debug::boldColor(Debug::Color::Default) |
||||
<< measurement._name << Debug::nospace << ":" << Debug::resetColor; |
||||
|
||||
/* If this measurement is not available yet, print a placeholder */ |
||||
if(_measuredFrameCount < Math::max(measurement._delay, 1u)) { |
||||
const char* units = nullptr; |
||||
switch(measurement._units) { |
||||
case Units::Count: |
||||
case Units::RatioThousandths: |
||||
units = ""; |
||||
break; |
||||
case Units::Nanoseconds: |
||||
units = "s"; |
||||
break; |
||||
case Units::Bytes: |
||||
units = "B"; |
||||
break; |
||||
case Units::PercentageThousandths: |
||||
units = "%"; |
||||
break; |
||||
} |
||||
CORRADE_INTERNAL_ASSERT(units); |
||||
|
||||
out << Debug::color(Debug::Color::Blue) << "-.--" |
||||
<< Debug::resetColor; |
||||
if(units[0] != '\0') out << units; |
||||
|
||||
/* Otherwise format the value */ |
||||
} else { |
||||
const Double mean = measurementDataInternal(measurement); |
||||
switch(measurement._units) { |
||||
case Units::Nanoseconds: |
||||
printTime(out, mean); |
||||
continue; |
||||
case Units::Bytes: |
||||
printCount(out, mean, 1024.0, "B"); |
||||
continue; |
||||
case Units::Count: |
||||
printCount(out, mean, 1000.0, ""); |
||||
continue; |
||||
case Units::RatioThousandths: |
||||
printCount(out, mean/1000.0, 1000.0, ""); |
||||
continue; |
||||
case Units::PercentageThousandths: |
||||
printValue(out, mean, 1000.0, " ", "%"); |
||||
continue; |
||||
} |
||||
|
||||
CORRADE_INTERNAL_ASSERT_UNREACHABLE(); /* LCOV_EXCL_LINE */ |
||||
} |
||||
} |
||||
} |
||||
|
||||
std::string FrameProfiler::statistics() const { |
||||
std::ostringstream out; |
||||
Debug d{&out, Debug::Flag::NoNewlineAtTheEnd|Debug::Flag::DisableColors}; |
||||
printStatisticsInternal(d); |
||||
return out.str(); |
||||
} |
||||
|
||||
void FrameProfiler::printStatistics(const std::size_t frequency) const { |
||||
Debug::Flags flags; |
||||
if(!Debug::isTty()) flags |= Debug::Flag::DisableColors; |
||||
printStatistics(Debug{flags}, frequency); |
||||
} |
||||
|
||||
void FrameProfiler::printStatistics(Debug& out, const std::size_t frequency) const { |
||||
if(!isEnabled() || _measuredFrameCount % frequency != 0) return; |
||||
|
||||
/* If on a TTY and we printed at least something already, scroll back up to
|
||||
overwrite previous output */ |
||||
if(out.isTty() && _measuredFrameCount > frequency) |
||||
out << Debug::nospace << "\033[" << Debug::nospace |
||||
<< _measurements.size() + 1 << Debug::nospace << "A\033[J" |
||||
<< Debug::nospace; |
||||
|
||||
printStatisticsInternal(out); |
||||
|
||||
/* Unconditionally finish with a newline so the TTY scrollback works
|
||||
correctly */ |
||||
if(out.flags() & Debug::Flag::NoNewlineAtTheEnd) |
||||
out << Debug::newline; |
||||
} |
||||
|
||||
Debug& operator<<(Debug& debug, const FrameProfiler::Units value) { |
||||
debug << "DebugTools::FrameProfiler::Units" << Debug::nospace; |
||||
|
||||
switch(value) { |
||||
/* LCOV_EXCL_START */ |
||||
#define _c(v) case FrameProfiler::Units::v: return debug << "::" #v; |
||||
_c(Nanoseconds) |
||||
_c(Bytes) |
||||
_c(Count) |
||||
_c(RatioThousandths) |
||||
_c(PercentageThousandths) |
||||
#undef _c |
||||
/* LCOV_EXCL_STOP */ |
||||
} |
||||
|
||||
return debug << "(" << Debug::nospace << reinterpret_cast<void*>(UnsignedByte(value)) << Debug::nospace << ")"; |
||||
} |
||||
|
||||
#ifdef MAGNUM_TARGET_GL |
||||
struct GLFrameProfiler::State { |
||||
UnsignedShort cpuDurationIndex = 0xffff, |
||||
gpuDurationIndex = 0xffff, |
||||
frameTimeIndex = 0xffff; |
||||
#ifndef MAGNUM_TARGET_GLES |
||||
UnsignedShort vertexFetchRatioIndex = 0xffff, |
||||
primitiveClipRatioIndex = 0xffff; |
||||
#endif |
||||
UnsignedLong frameTimeStartFrame[2]; |
||||
UnsignedLong cpuDurationStartFrame; |
||||
GL::TimeQuery timeQueries[3]{GL::TimeQuery{NoCreate}, GL::TimeQuery{NoCreate}, GL::TimeQuery{NoCreate}}; |
||||
#ifndef MAGNUM_TARGET_GLES |
||||
GL::PipelineStatisticsQuery verticesSubmittedQueries[3]{GL::PipelineStatisticsQuery{NoCreate}, GL::PipelineStatisticsQuery{NoCreate}, GL::PipelineStatisticsQuery{NoCreate}}; |
||||
GL::PipelineStatisticsQuery vertexShaderInvocationsQueries[3]{GL::PipelineStatisticsQuery{NoCreate}, GL::PipelineStatisticsQuery{NoCreate}, GL::PipelineStatisticsQuery{NoCreate}}; |
||||
GL::PipelineStatisticsQuery clippingInputPrimitivesQueries[3]{GL::PipelineStatisticsQuery{NoCreate}, GL::PipelineStatisticsQuery{NoCreate}, GL::PipelineStatisticsQuery{NoCreate}}; |
||||
GL::PipelineStatisticsQuery clippingOutputPrimitivesQueries[3]{GL::PipelineStatisticsQuery{NoCreate}, GL::PipelineStatisticsQuery{NoCreate}, GL::PipelineStatisticsQuery{NoCreate}}; |
||||
#endif |
||||
}; |
||||
|
||||
GLFrameProfiler::GLFrameProfiler(): _state{Containers::InPlaceInit} {} |
||||
|
||||
GLFrameProfiler::GLFrameProfiler(const Values values, const std::size_t maxFrameCount): GLFrameProfiler{} |
||||
{ |
||||
setup(values, maxFrameCount); |
||||
} |
||||
|
||||
GLFrameProfiler::GLFrameProfiler(GLFrameProfiler&&) noexcept = default; |
||||
|
||||
GLFrameProfiler& GLFrameProfiler::operator=(GLFrameProfiler&&) noexcept = default; |
||||
|
||||
GLFrameProfiler::~GLFrameProfiler() = default; |
||||
|
||||
void GLFrameProfiler::setup(const Values values, const std::size_t maxFrameCount) { |
||||
UnsignedShort index = 0; |
||||
Containers::Array<Measurement> measurements; |
||||
if(values & Value::FrameTime) { |
||||
/* Fucking hell, STL. When I first saw std::chrono back in 2010 I
|
||||
should have flipped the table and learn carpentry instead. BUT NO, |
||||
I'm still suffering this abomination a decade later! */ |
||||
arrayAppend(measurements, Containers::InPlaceInit, |
||||
"Frame time", Units::Nanoseconds, UnsignedInt(Containers::arraySize(_state->frameTimeStartFrame)), |
||||
[](void* state, UnsignedInt current) { |
||||
static_cast<State*>(state)->frameTimeStartFrame[current] = std::chrono::duration_cast<std::chrono::nanoseconds>(std::chrono::high_resolution_clock::now().time_since_epoch()).count(); |
||||
}, |
||||
[](void*, UnsignedInt) {}, |
||||
[](void* state, UnsignedInt previous, UnsignedInt current) { |
||||
auto& self = *static_cast<State*>(state); |
||||
return self.frameTimeStartFrame[current] - |
||||
self.frameTimeStartFrame[previous]; |
||||
}, _state.get()); |
||||
_state->frameTimeIndex = index++; |
||||
} |
||||
if(values & Value::CpuDuration) { |
||||
arrayAppend(measurements, Containers::InPlaceInit, |
||||
"CPU duration", Units::Nanoseconds, |
||||
[](void* state) { |
||||
static_cast<State*>(state)->cpuDurationStartFrame = std::chrono::duration_cast<std::chrono::nanoseconds>(std::chrono::high_resolution_clock::now().time_since_epoch()).count(); |
||||
}, |
||||
[](void* state) { |
||||
return std::chrono::duration_cast<std::chrono::nanoseconds>(std::chrono::high_resolution_clock::now().time_since_epoch()).count() - static_cast<State*>(state)->cpuDurationStartFrame; |
||||
}, _state.get()); |
||||
_state->cpuDurationIndex = index++; |
||||
} |
||||
if(values & Value::GpuDuration) { |
||||
for(GL::TimeQuery& q: _state->timeQueries) |
||||
q = GL::TimeQuery{GL::TimeQuery::Target::TimeElapsed}; |
||||
arrayAppend(measurements, Containers::InPlaceInit, |
||||
"GPU duration", Units::Nanoseconds, |
||||
UnsignedInt(Containers::arraySize(_state->timeQueries)), |
||||
[](void* state, UnsignedInt current) { |
||||
static_cast<State*>(state)->timeQueries[current].begin(); |
||||
}, |
||||
[](void* state, UnsignedInt current) { |
||||
static_cast<State*>(state)->timeQueries[current].end(); |
||||
}, |
||||
[](void* state, UnsignedInt previous, UnsignedInt) { |
||||
return static_cast<State*>(state)->timeQueries[previous].result<UnsignedLong>(); |
||||
}, _state.get()); |
||||
_state->gpuDurationIndex = index++; |
||||
} |
||||
#ifndef MAGNUM_TARGET_GLES |
||||
if(values & Value::VertexFetchRatio) { |
||||
for(GL::PipelineStatisticsQuery& q: _state->verticesSubmittedQueries) |
||||
q = GL::PipelineStatisticsQuery{GL::PipelineStatisticsQuery::Target::VerticesSubmitted}; |
||||
for(GL::PipelineStatisticsQuery& q: _state->vertexShaderInvocationsQueries) |
||||
q = GL::PipelineStatisticsQuery{GL::PipelineStatisticsQuery::Target::VertexShaderInvocations}; |
||||
arrayAppend(measurements, Containers::InPlaceInit, |
||||
"Vertex fetch ratio", Units::RatioThousandths, |
||||
UnsignedInt(Containers::arraySize(_state->verticesSubmittedQueries)), |
||||
[](void* state, UnsignedInt current) { |
||||
static_cast<State*>(state)->verticesSubmittedQueries[current].begin(); |
||||
static_cast<State*>(state)->vertexShaderInvocationsQueries[current].begin(); |
||||
}, |
||||
[](void* state, UnsignedInt current) { |
||||
static_cast<State*>(state)->verticesSubmittedQueries[current].end(); |
||||
static_cast<State*>(state)->vertexShaderInvocationsQueries[current].end(); |
||||
}, |
||||
[](void* state, UnsignedInt previous, UnsignedInt) { |
||||
/* Avoid division by zero if a frame doesn't have any draws */ |
||||
const auto submitted = static_cast<State*>(state)->verticesSubmittedQueries[previous].result<UnsignedLong>(); |
||||
if(!submitted) return UnsignedLong{}; |
||||
|
||||
return static_cast<State*>(state)->vertexShaderInvocationsQueries[previous].result<UnsignedLong>()*1000/submitted; |
||||
}, _state.get()); |
||||
_state->vertexFetchRatioIndex = index++; |
||||
} |
||||
if(values & Value::PrimitiveClipRatio) { |
||||
for(GL::PipelineStatisticsQuery& q: _state->clippingInputPrimitivesQueries) |
||||
q = GL::PipelineStatisticsQuery{GL::PipelineStatisticsQuery::Target::ClippingInputPrimitives}; |
||||
for(GL::PipelineStatisticsQuery& q: _state->clippingOutputPrimitivesQueries) |
||||
q = GL::PipelineStatisticsQuery{GL::PipelineStatisticsQuery::Target::ClippingOutputPrimitives}; |
||||
arrayAppend(measurements, Containers::InPlaceInit, |
||||
"Primitives clipped", Units::PercentageThousandths, |
||||
UnsignedInt(Containers::arraySize(_state->clippingInputPrimitivesQueries)), |
||||
[](void* state, UnsignedInt current) { |
||||
static_cast<State*>(state)->clippingInputPrimitivesQueries[current].begin(); |
||||
static_cast<State*>(state)->clippingOutputPrimitivesQueries[current].begin(); |
||||
}, |
||||
[](void* state, UnsignedInt current) { |
||||
static_cast<State*>(state)->clippingInputPrimitivesQueries[current].end(); |
||||
static_cast<State*>(state)->clippingOutputPrimitivesQueries[current].end(); |
||||
}, |
||||
[](void* state, UnsignedInt previous, UnsignedInt) { |
||||
/* Avoid division by zero if a frame doesn't have any draws */ |
||||
const auto input = static_cast<State*>(state)->clippingInputPrimitivesQueries[previous].result<UnsignedLong>(); |
||||
if(!input) return UnsignedLong{}; |
||||
|
||||
return 100000 - static_cast<State*>(state)->clippingOutputPrimitivesQueries[previous].result<UnsignedLong>()*100000/input; |
||||
}, _state.get()); |
||||
_state->primitiveClipRatioIndex = index++; |
||||
} |
||||
#endif |
||||
setup(std::move(measurements), maxFrameCount); |
||||
} |
||||
|
||||
auto GLFrameProfiler::values() const -> Values { |
||||
Values values; |
||||
if(_state->frameTimeIndex != 0xffff) values |= Value::FrameTime; |
||||
if(_state->cpuDurationIndex != 0xffff) values |= Value::CpuDuration; |
||||
if(_state->gpuDurationIndex != 0xffff) values |= Value::GpuDuration; |
||||
#ifndef MAGNUM_TARGET_GLES |
||||
if(_state->vertexFetchRatioIndex != 0xffff) values |= Value::VertexFetchRatio; |
||||
if(_state->primitiveClipRatioIndex != 0xffff) values |= Value::PrimitiveClipRatio; |
||||
#endif |
||||
return values; |
||||
} |
||||
|
||||
bool GLFrameProfiler::isMeasurementAvailable(const Value value) const { |
||||
const UnsignedShort* index = nullptr; |
||||
switch(value) { |
||||
case Value::FrameTime: index = &_state->frameTimeIndex; break; |
||||
case Value::CpuDuration: index = &_state->cpuDurationIndex; break; |
||||
case Value::GpuDuration: index = &_state->gpuDurationIndex; break; |
||||
#ifndef MAGNUM_TARGET_GLES |
||||
case Value::VertexFetchRatio: index = &_state->vertexFetchRatioIndex; break; |
||||
case Value::PrimitiveClipRatio: index = &_state->primitiveClipRatioIndex; break; |
||||
#endif |
||||
} |
||||
CORRADE_INTERNAL_ASSERT(index); |
||||
CORRADE_ASSERT(*index < measurementCount(), |
||||
"DebugTools::GLFrameProfiler::isMeasurementAvailable():" << value << "not enabled", {}); |
||||
return isMeasurementAvailable(*index); |
||||
} |
||||
|
||||
Double GLFrameProfiler::frameTimeMean() const { |
||||
CORRADE_ASSERT(_state->frameTimeIndex < measurementCount(), |
||||
"DebugTools::GLFrameProfiler::frameTimeMean(): not enabled", {}); |
||||
return measurementMean(_state->frameTimeIndex); |
||||
} |
||||
|
||||
Double GLFrameProfiler::cpuDurationMean() const { |
||||
CORRADE_ASSERT(_state->cpuDurationIndex < measurementCount(), |
||||
"DebugTools::GLFrameProfiler::cpuDurationMean(): not enabled", {}); |
||||
return measurementMean(_state->cpuDurationIndex); |
||||
} |
||||
|
||||
Double GLFrameProfiler::gpuDurationMean() const { |
||||
CORRADE_ASSERT(_state->gpuDurationIndex < measurementCount(), |
||||
"DebugTools::GLFrameProfiler::gpuDurationMean(): not enabled", {}); |
||||
return measurementMean(_state->gpuDurationIndex); |
||||
} |
||||
|
||||
#ifndef MAGNUM_TARGET_GLES |
||||
Double GLFrameProfiler::vertexFetchRatioMean() const { |
||||
CORRADE_ASSERT(_state->vertexFetchRatioIndex < measurementCount(), |
||||
"DebugTools::GLFrameProfiler::vertexFetchRatioMean(): not enabled", {}); |
||||
return measurementMean(_state->vertexFetchRatioIndex); |
||||
} |
||||
|
||||
Double GLFrameProfiler::primitiveClipRatioMean() const { |
||||
CORRADE_ASSERT(_state->primitiveClipRatioIndex < measurementCount(), |
||||
"DebugTools::GLFrameProfiler::primitiveClipRatioMean(): not enabled", {}); |
||||
return measurementMean(_state->primitiveClipRatioIndex); |
||||
} |
||||
#endif |
||||
|
||||
Debug& operator<<(Debug& debug, const GLFrameProfiler::Value value) { |
||||
debug << "DebugTools::GLFrameProfiler::Value" << Debug::nospace; |
||||
|
||||
switch(value) { |
||||
/* LCOV_EXCL_START */ |
||||
#define _c(v) case GLFrameProfiler::Value::v: return debug << "::" #v; |
||||
_c(FrameTime) |
||||
_c(CpuDuration) |
||||
_c(GpuDuration) |
||||
#ifndef MAGNUM_TARGET_GLES |
||||
_c(VertexFetchRatio) |
||||
_c(PrimitiveClipRatio) |
||||
#endif |
||||
#undef _c |
||||
/* LCOV_EXCL_STOP */ |
||||
} |
||||
|
||||
return debug << "(" << Debug::nospace << reinterpret_cast<void*>(UnsignedShort(value)) << Debug::nospace << ")"; |
||||
} |
||||
|
||||
Debug& operator<<(Debug& debug, const GLFrameProfiler::Values value) { |
||||
return Containers::enumSetDebugOutput(debug, value, "DebugTools::GLFrameProfiler::Values{}", { |
||||
GLFrameProfiler::Value::FrameTime, |
||||
GLFrameProfiler::Value::CpuDuration, |
||||
GLFrameProfiler::Value::GpuDuration, |
||||
#ifndef MAGNUM_TARGET_GLES |
||||
GLFrameProfiler::Value::VertexFetchRatio, |
||||
GLFrameProfiler::Value::PrimitiveClipRatio |
||||
#endif |
||||
}); |
||||
} |
||||
#endif |
||||
|
||||
}} |
||||
@ -0,0 +1,704 @@
|
||||
#ifndef Magnum_DebugTools_FrameProfiler_h |
||||
#define Magnum_DebugTools_FrameProfiler_h |
||||
/*
|
||||
This file is part of Magnum. |
||||
|
||||
Copyright © 2010, 2011, 2012, 2013, 2014, 2015, 2016, 2017, 2018, 2019 |
||||
Vladimír Vondruš <mosra@centrum.cz> |
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a |
||||
copy of this software and associated documentation files (the "Software"), |
||||
to deal in the Software without restriction, including without limitation |
||||
the rights to use, copy, modify, merge, publish, distribute, sublicense, |
||||
and/or sell copies of the Software, and to permit persons to whom the |
||||
Software is furnished to do so, subject to the following conditions: |
||||
|
||||
The above copyright notice and this permission notice shall be included |
||||
in all copies or substantial portions of the Software. |
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR |
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, |
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL |
||||
THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER |
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING |
||||
FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER |
||||
DEALINGS IN THE SOFTWARE. |
||||
*/ |
||||
|
||||
/** @file
|
||||
* @brief Class @ref Magnum::DebugTools::FrameProfiler, @ref Magnum::DebugTools::GLFrameProfiler |
||||
* @m_since_latest |
||||
*/ |
||||
|
||||
#include <string> |
||||
#include <Corrade/Containers/Array.h> |
||||
#include <Corrade/Containers/Pointer.h> |
||||
|
||||
#include "Magnum/Magnum.h" |
||||
#include "Magnum/DebugTools/visibility.h" |
||||
|
||||
namespace Magnum { namespace DebugTools { |
||||
|
||||
/**
|
||||
@brief Frame profiler |
||||
@m_since_latest |
||||
|
||||
A generic implementation of a frame profiler supporting a moving average over |
||||
a set of frames as well as delayed measurements to avoid stalls when querying |
||||
the results. This class alone doesn't provide any pre-defined measurements, see |
||||
for example @ref GLFrameProfiler that provides common measurements like CPU and |
||||
GPU time. |
||||
|
||||
@experimental |
||||
|
||||
@section DebugTools-FrameProfiler-usage Basic usage |
||||
|
||||
Measurements are performed by calling @ref beginFrame() and @ref endFrame() at |
||||
designated points in the frame: |
||||
|
||||
@snippet MagnumDebugTools.cpp FrameProfiler-usage |
||||
|
||||
In order to have stable profiling results, the application needs to redraw |
||||
constantly. However for applications that otherwise redraw only on change it |
||||
might be wasteful --- to account for this, it's possible to toggle the profiler |
||||
operation using @ref enable() / @ref disable() and then |
||||
@ref Platform::Sdl2Application::redraw() "redraw()" can be called only if |
||||
@ref isEnabled() returns @cpp true @ce. |
||||
|
||||
Data for all measurements is then available through @ref measurementName(), |
||||
@ref measurementUnits() and @ref measurementMean(). For a convenient overview |
||||
of all measured values you can call @ref statistics() and feed its output to a |
||||
UI library or something that can render text. Alternatively, if you don't want |
||||
to bother with text rendering, call @ref printStatistics() to have the output |
||||
periodically printed to the console. If an interactive terminal is detected, |
||||
the output will be colored and refreshing itself in place. Together with the |
||||
on-demand profiling, it could look like this, refreshing the output every 10 |
||||
frames: |
||||
|
||||
@snippet MagnumDebugTools.cpp FrameProfiler-usage-console |
||||
|
||||
And here's a sample output on the terminal --- using a fully configured |
||||
@link GLFrameProfiler @endlink: |
||||
|
||||
@include debugtools-frameprofiler.ansi |
||||
|
||||
@section DebugTools-FrameProfiler-setup Setting up measurements |
||||
|
||||
Unless you're using this class through @ref GLFrameProfiler, measurements |
||||
have to be set up by passing @ref Measurement instances to the @ref setup() |
||||
function or to the constructor, together with specifying count of frames for |
||||
the moving average. A CPU duration measurements using the @ref std::chrono APIs |
||||
over last 50 frames can be done like this: |
||||
|
||||
@snippet MagnumDebugTools.cpp FrameProfiler-setup-immediate |
||||
|
||||
In the above case, the measurement result is available immediately on frame |
||||
end. That's not always the case, and for example GPU queries need a few frames |
||||
delay to avoid stalls from CPU/GPU synchronization. The following snippet sets |
||||
up sample count measurement with a delay, using three separate |
||||
@ref GL::SampleQuery instances that are cycled through each frame and retrieved |
||||
two frames later. The profiler automatically takes care of choosing one of the |
||||
three instances for each measurement via additional `current` / `previous` |
||||
parameters passed to each callback: |
||||
|
||||
@snippet MagnumDebugTools-gl.cpp FrameProfiler-setup-delayed |
||||
|
||||
<b></b> |
||||
|
||||
@m_class{m-block m-warning} |
||||
|
||||
@par Move construction and state pointers in callbacks |
||||
The @ref FrameProfiler class is movable, which could potentially mean that |
||||
state pointers passed to callback functions become dangling. It's not a |
||||
problem in the above snippets because the state is always external to the |
||||
instance, but care has to be taken when passing pointers to subclasses. |
||||
@par |
||||
When setting up measurements from a subclass, it's recommended to always |
||||
pass @cpp this @ce as the state pointer. The base class recognizes it |
||||
during a move and patches the state to point to the new instance instead. |
||||
If you don't or can't use @cpp this @ce as a state pointer, you need to |
||||
either provide a dedicated move constructor and assignment to do the |
||||
required patching or disable moves altogether to avoid accidents. |
||||
*/ |
||||
class MAGNUM_DEBUGTOOLS_EXPORT FrameProfiler { |
||||
public: |
||||
/**
|
||||
* @brief Measurement units |
||||
* |
||||
* @see @ref Measurement |
||||
*/ |
||||
enum class Units: UnsignedByte { |
||||
/**
|
||||
* Nanoseconds, measuring for example elapsed time. Depending on |
||||
* the magnitude, @ref statistics() can show them as microseconds, |
||||
* milliseconds or seconds. |
||||
*/ |
||||
Nanoseconds, |
||||
|
||||
/**
|
||||
* Bytes, measuring for example memory usage, bandwidth. Depending |
||||
* on the magnitude, @ref statistics() can show them as kB, MB, GB |
||||
* (with a multiplier of 1024). |
||||
*/ |
||||
Bytes, |
||||
|
||||
/**
|
||||
* Generic count. For discrete values that don't fit any of the |
||||
* above. Depending on the magnitude, @ref statistics() can show |
||||
* the value as k, M or G (with a multiplier of 1000). |
||||
*/ |
||||
Count, |
||||
|
||||
/**
|
||||
* Ratio expressed in 1/1000s. @ref statistics() divides the value |
||||
* by 1000 and depending on the magnitude it can show it also as k, |
||||
* M or G (with a multiplier of 1000). |
||||
*/ |
||||
RatioThousandths, |
||||
|
||||
/**
|
||||
* Percentage expressed in 1/1000s. @ref statistics() divides the |
||||
* value by 1000 and appends a % sign. |
||||
*/ |
||||
PercentageThousandths |
||||
}; |
||||
|
||||
class Measurement; |
||||
|
||||
/**
|
||||
* @brief Default constructor |
||||
* |
||||
* Call @ref setup() to populate the profiler with measurements. |
||||
*/ |
||||
explicit FrameProfiler() noexcept; |
||||
|
||||
/**
|
||||
* @brief Constructor |
||||
* |
||||
* Equivalent to default-constructing an instance and calling |
||||
* @ref setup() afterwards. |
||||
*/ |
||||
explicit FrameProfiler(Containers::Array<Measurement>&& measurements, std::size_t maxFrameCount) noexcept; |
||||
|
||||
/** @overload */ |
||||
explicit FrameProfiler(std::initializer_list<Measurement> measurements, std::size_t maxFrameCount); |
||||
|
||||
/** @brief Copying is not allowed */ |
||||
FrameProfiler(const FrameProfiler&) = delete; |
||||
|
||||
/** @brief Move constructor */ |
||||
FrameProfiler(FrameProfiler&&) noexcept; |
||||
|
||||
/** @brief Copying is not allowed */ |
||||
FrameProfiler& operator=(const FrameProfiler&) = delete; |
||||
|
||||
/** @brief Move assignment */ |
||||
FrameProfiler& operator=(FrameProfiler&&) noexcept; |
||||
|
||||
/**
|
||||
* @brief Setup measurements |
||||
* @param measurements List of measurements |
||||
* @param maxFrameCount Max frame count over which to calculate a |
||||
* moving average. Expected to be at least @cpp 1 @ce. |
||||
* |
||||
* Calling @ref setup() on an already set up profiler will replace |
||||
* existing measurements with @p measurements and reset |
||||
* @ref measuredFrameCount() back to @cpp 0 @ce. |
||||
*/ |
||||
void setup(Containers::Array<Measurement>&& measurements, std::size_t maxFrameCount); |
||||
|
||||
/** @overload */ |
||||
void setup(std::initializer_list<Measurement> measurements, std::size_t maxFrameCount); |
||||
|
||||
/** @brief Whether the profiling is enabled */ |
||||
bool isEnabled() const { return _enabled; } |
||||
|
||||
/**
|
||||
* @brief Enable the profiler |
||||
* |
||||
* The profiler is enabled implicitly after construction. When this |
||||
* function is called, it discards all measured data, effectively |
||||
* making @ref measuredFrameCount() zero. If you want to reset the |
||||
* profiler to measure different values as well, call @ref setup(). |
||||
*/ |
||||
void enable(); |
||||
|
||||
/**
|
||||
* @brief Disable the profiler |
||||
* |
||||
* Disabling the profiler will make @ref beginFrame() and |
||||
* @ref endFrame() a no-op, effectively freezing all reported |
||||
* measurements until the profiler is enabled again. |
||||
*/ |
||||
void disable(); |
||||
|
||||
/**
|
||||
* @brief Begin a frame |
||||
* |
||||
* Has to be called at the beginning of a frame and be paired with a |
||||
* corresponding @ref endFrame(). Calls @p begin functions in all |
||||
* @ref Measurement instances passed to @ref setup(). If the profiler |
||||
* is disabled, the function is a no-op. |
||||
* @see @ref isEnabled() |
||||
*/ |
||||
void beginFrame(); |
||||
|
||||
/**
|
||||
* @brief End a frame |
||||
* |
||||
* Has to be called at the end of frame, before buffer swap, and be |
||||
* paired with a corresponding @ref beginFrame(). Calls @p end |
||||
* functions in all @ref Measurement instances passed to @ref setup() |
||||
* and @p query functions on all measurements that are sufficiently |
||||
* delayed, saving their output. If the profiler is disabled, the |
||||
* function is a no-op. |
||||
* @see @ref isEnabled() |
||||
*/ |
||||
void endFrame(); |
||||
|
||||
/**
|
||||
* @brief Max count of measured frames |
||||
* |
||||
* How many frames to calculate a moving average from. At the beginning |
||||
* of a measurement when there's not enough frames, the average is |
||||
* calculated only from @ref measuredFrameCount(). Always at least |
||||
* @cpp 1 @ce. |
||||
*/ |
||||
std::size_t maxFrameCount() const { return _maxFrameCount; } |
||||
|
||||
/**
|
||||
* @brief Count of measured frames |
||||
* |
||||
* Count of times @ref endFrame() was called, but at most |
||||
* @ref maxFrameCount(), after which the profiler calculates a moving |
||||
* average over last @ref maxFrameCount() frames only. Actual data |
||||
* availability depends on @ref measurementDelay(). |
||||
*/ |
||||
std::size_t measuredFrameCount() const { return _measuredFrameCount; } |
||||
|
||||
/**
|
||||
* @brief Measurement count |
||||
* |
||||
* Count of @ref Measurement instances passed to @ref setup(). If |
||||
* @ref setup() was not called yet, returns @cpp 0 @ce. |
||||
*/ |
||||
std::size_t measurementCount() const { return _measurements.size(); } |
||||
|
||||
/**
|
||||
* @brief Measurement name |
||||
* |
||||
* The @p id corresponds to the index of the measurement in the list |
||||
* passed to @ref setup(). Expects that @p id is less than |
||||
* @ref measurementCount(). |
||||
*/ |
||||
std::string measurementName(std::size_t id) const; |
||||
|
||||
/**
|
||||
* @brief Measurement units |
||||
* |
||||
* The @p id corresponds to the index of the measurement in the list |
||||
* passed to @ref setup(). Expects that @p id is less than |
||||
* @ref measurementCount(). |
||||
*/ |
||||
Units measurementUnits(std::size_t id) const; |
||||
|
||||
/**
|
||||
* @brief Measurement delay |
||||
* |
||||
* How many @ref beginFrame() / @ref endFrame() call pairs needs to be |
||||
* performed before a value for given measurement is available. Always |
||||
* at least @cpp 1 @ce. The @p id corresponds to the index of the |
||||
* measurement in the list passed to @ref setup(). Expects that @p id |
||||
* is less than @ref measurementCount(). |
||||
*/ |
||||
UnsignedInt measurementDelay(std::size_t id) const; |
||||
|
||||
/**
|
||||
* @brief Whether given measurement is available |
||||
* |
||||
* Returns @cpp true @ce if @ref measuredFrameCount() is at least |
||||
* @ref measurementDelay() for given @p id, @cpp false @ce otherwise. |
||||
* The @p id corresponds to the index of the measurement in the list |
||||
* passed to @ref setup(). Expects that @p id is less than |
||||
* @ref measurementCount(). |
||||
*/ |
||||
bool isMeasurementAvailable(std::size_t id) const; |
||||
|
||||
/**
|
||||
* @brief Measurement mean |
||||
* |
||||
* Returns a moving average of @f$ n @f$ previous measurements out of |
||||
* the total @f$ M @f$ values: @f[ |
||||
* \bar{x}_\text{SM} = \dfrac{1}{n} \sum\limits_{i=0}^{n-1} x_{M -i} |
||||
* @f] |
||||
* |
||||
* The @p id corresponds to the index of the measurement in the list |
||||
* passed to @ref setup(). Expects that @p id is less than |
||||
* @ref measurementCount() and that the measurement is available. |
||||
* @see @ref isMeasurementAvailable() |
||||
*/ |
||||
Double measurementMean(std::size_t id) const; |
||||
|
||||
/**
|
||||
* @brief Overview of all measurements |
||||
* |
||||
* Returns a formatted string with names, means and units of all |
||||
* measurements in the order they were added. If some measurement data |
||||
* is available yet, prints placeholder values for these; if the |
||||
* @see @ref isMeasurementAvailable(), @ref isEnabled() |
||||
*/ |
||||
std::string statistics() const; |
||||
|
||||
/**
|
||||
* @brief Print an overview of all measurements to a console at given rate |
||||
* |
||||
* Expected to be called every frame. On every `frequency`th frame |
||||
* prints the same information as @ref statistics(), but in addition, |
||||
* if the output is a TTY, it's colored and overwrites itself instead |
||||
* of filling up the terminal history. |
||||
* @see @ref isMeasurementAvailable(), @ref isEnabled() |
||||
* @ref Corrade::Utility::Debug::isTty() |
||||
*/ |
||||
void printStatistics(std::size_t frequency) const; |
||||
|
||||
/**
|
||||
* @brief Print an overview of all measurements to given output at given rate |
||||
* |
||||
* Compared to @ref printStatistics(std::size_t) const prints to given |
||||
* @p out (which can be also @ref Corrade::Utility::Warning or |
||||
* @ref Corrade::Utility::Error) and uses it to decide whether the |
||||
* output is a TTY and whether to print colors. |
||||
* @see @ref Corrade::Utility::Debug::isTty(), |
||||
* @ref Corrade::Utility::Debug::Flag::DisableColors |
||||
*/ |
||||
void printStatistics(Debug& out, std::size_t frequency) const; |
||||
|
||||
/** @overload */ |
||||
void printStatistics(Debug&& out, std::size_t frequency) const { |
||||
printStatistics(out, frequency); |
||||
} |
||||
|
||||
private: |
||||
std::size_t delayedCurrentData(UnsignedInt delay) const; |
||||
Double measurementDataInternal(const Measurement& measurement) const; |
||||
void printStatisticsInternal(Debug& out) const; |
||||
|
||||
bool _enabled = true; |
||||
#ifndef CORRADE_NO_ASSERT |
||||
/* Here it shouldn't cause the class to have a different size when
|
||||
asserts get disabled */ |
||||
bool _beginFrameCalled{}; |
||||
#endif |
||||
std::size_t _currentData{}, _maxFrameCount{1}, _measuredFrameCount{}; |
||||
Containers::Array<Measurement> _measurements; |
||||
Containers::Array<UnsignedLong> _data; |
||||
}; |
||||
|
||||
/**
|
||||
@brief Measurement |
||||
|
||||
Describes a single measurement passed to @ref FrameProfiler::setup(). See |
||||
@ref DebugTools-FrameProfiler-setup for introduction and examples. |
||||
*/ |
||||
class MAGNUM_DEBUGTOOLS_EXPORT FrameProfiler::Measurement { |
||||
public: |
||||
/**
|
||||
* @brief Construct an immediate measurement |
||||
* @param name Measurement name, used in |
||||
* @ref FrameProfiler::measurementName() and |
||||
* @ref FrameProfiler::statistics() |
||||
* @param units Measurement units, used in |
||||
* @ref FrameProfiler::measurementUnits() and |
||||
* @ref FrameProfiler::statistics() |
||||
* @param begin Function to call at the beginning of a frame |
||||
* @param end Function to call at the end of a frame, returning |
||||
* the measured value |
||||
* @param state State pointer passed to both @p begin and @p end |
||||
* as a first argument |
||||
*/ |
||||
explicit Measurement(const std::string& name, Units units, void(*begin)(void*), UnsignedLong(*end)(void*), void* state); |
||||
|
||||
/**
|
||||
* @brief Construct a delayed measurement |
||||
* @param name Measurement name, used in |
||||
* @ref FrameProfiler::measurementName() and |
||||
* @ref FrameProfiler::statistics() |
||||
* @param units Measurement units, used in |
||||
* @ref FrameProfiler::measurementUnits() and |
||||
* @ref FrameProfiler::statistics() |
||||
* @param delay How many @ref FrameProfiler::endFrame() calls has |
||||
* to happen before a measured value can be retrieved using |
||||
* @p query. Has to be at least @cpp 1 @ce, delay of @cpp 1 @ce is |
||||
* equal in behavior to immediate measurements. |
||||
* @param begin Function to call at the beginning of a frame. |
||||
* Second argument is a `current` index that's guaranteed to be |
||||
* less than @p delay and always different in each consecutive |
||||
* call. |
||||
* @param end Function to call at the end of a frame. |
||||
* Second argument is a `current` index that's guaranteed to be |
||||
* less than @p delay and always different in each consecutive |
||||
* call. |
||||
* @param query Function to call to get the measured value. Called |
||||
* after @p delay frames. First argument is a `previous` index |
||||
* that is the same as the `current` argument passed to a |
||||
* corresponding @p begin / @p end function of the measurement to |
||||
* query the value of. Second argument is a `current` index that |
||||
* corresponds to current frame. |
||||
* @param state State pointer passed to both @p begin and @p end |
||||
* as a first argument |
||||
*/ |
||||
explicit Measurement(const std::string& name, Units units, UnsignedInt delay, void(*begin)(void*, UnsignedInt), void(*end)(void*, UnsignedInt), UnsignedLong(*query)(void*, UnsignedInt, UnsignedInt), void* state); |
||||
|
||||
private: |
||||
friend FrameProfiler; |
||||
|
||||
std::string _name; |
||||
union { |
||||
void(*immediate)(void*); |
||||
void(*delayed)(void*, UnsignedInt); |
||||
} _begin; |
||||
void(*_end)(void*, UnsignedInt); |
||||
union { |
||||
UnsignedLong(*immediate)(void*); |
||||
UnsignedLong(*delayed)(void*, UnsignedInt, UnsignedInt); |
||||
} _query; |
||||
void* _state; |
||||
Units _units; |
||||
/* Set to 0 to distinguish immediate measurements (first
|
||||
constructor), however always used as max(_delay, 1) */ |
||||
UnsignedInt _delay; |
||||
|
||||
UnsignedInt _current{}; |
||||
UnsignedLong _movingSum{}; |
||||
}; |
||||
|
||||
/**
|
||||
@debugoperatorclassenum{FrameProfiler,FrameProfiler::Units} |
||||
@m_since_latest |
||||
*/ |
||||
MAGNUM_DEBUGTOOLS_EXPORT Debug& operator<<(Debug& debug, FrameProfiler::Units value); |
||||
|
||||
#ifdef MAGNUM_TARGET_GL |
||||
/**
|
||||
@brief OpenGL frame profiler |
||||
@m_since_latest |
||||
|
||||
A @ref FrameProfiler with OpenGL-specific measurements. Instantiate with a |
||||
desired subset of measured values and then continue the same way as described |
||||
in the @ref DebugTools-FrameProfiler-usage "FrameProfiler usage documentation": |
||||
|
||||
@snippet MagnumDebugTools-gl.cpp GLFrameProfiler-usage |
||||
|
||||
If none if @ref Value::GpuDuration, @ref Value::VertexFetchRatio and |
||||
@ref Value::PrimitiveClipRatio is not enabled, the class can operate without an |
||||
active OpenGL context. |
||||
|
||||
@experimental |
||||
*/ |
||||
class MAGNUM_DEBUGTOOLS_EXPORT GLFrameProfiler: public FrameProfiler { |
||||
public: |
||||
/**
|
||||
* @brief Measured value |
||||
* |
||||
* @see @ref Values, @ref GLFrameProfiler(Values, std::size_t), |
||||
* @ref setup() |
||||
*/ |
||||
enum class Value: UnsignedShort { |
||||
/**
|
||||
* Measure total frame time (i.e., time between consecutive |
||||
* @ref beginFrame() calls). Reported in @ref Units::Nanoseconds |
||||
* with a delay of 2 frames. When converted to seconds, the value |
||||
* is an inverse of FPS. |
||||
*/ |
||||
FrameTime = 1 << 0, |
||||
|
||||
/**
|
||||
* Measure CPU frame duration (i.e., CPU time spent between |
||||
* @ref beginFrame() and @ref endFrame()). Reported in |
||||
* @ref Units::Nanoseconds with a delay of 1 frame. |
||||
*/ |
||||
CpuDuration = 1 << 1, |
||||
|
||||
/**
|
||||
* Measure GPU frame duration (i.e., time between @ref beginFrame() |
||||
* and @ref endFrame()). Reported in @ref Units::Nanoseconds |
||||
* with a delay of 3 frames. This value requires an active OpenGL |
||||
* context. |
||||
* @requires_gl33 Extension @gl_extension{ARB,timer_query} |
||||
* @requires_es_extension Extension @gl_extension{EXT,disjoint_timer_query} |
||||
* @requires_webgl_extension Extension @webgl_extension{EXT,disjoint_timer_query} |
||||
* on WebGL 1, @gl_extension{EXT,disjoint_timer_query_webgl2} |
||||
* on WebGL 2 |
||||
*/ |
||||
GpuDuration = 1 << 2, |
||||
|
||||
#ifndef MAGNUM_TARGET_GLES |
||||
/**
|
||||
* Ratio of vertex shader invocations to count of vertices |
||||
* submitted. For a non-indexed draw the ratio will be 1, for |
||||
* indexed draws ratio is less than 1. The lower the value is, the |
||||
* better a mesh is optimized for post-transform vertex cache. |
||||
* Reported in @ref Units::RatioThousandths with a delay of 3 |
||||
* frames. This value requires an active OpenGL context. |
||||
* @requires_gl46 Extension @gl_extension{ARB,pipeline_statistics_query} |
||||
*/ |
||||
VertexFetchRatio = 1 << 3, |
||||
|
||||
/**
|
||||
* Ratio of primitives discarded by the clipping stage to count of |
||||
* primitives submitted. The ratio is 0 when all primitives pass |
||||
* the clipping stage and 1 when all are discarded. Can be used to |
||||
* measure efficiency of a frustum culling algorithm. Reported in |
||||
* @ref Units::PercentageThousandths with a delay of 3 frames. This |
||||
* value requires an active OpenGL context. |
||||
* @requires_gl46 Extension @gl_extension{ARB,pipeline_statistics_query} |
||||
*/ |
||||
PrimitiveClipRatio = 1 << 4 |
||||
#endif |
||||
}; |
||||
|
||||
/**
|
||||
* @brief Measured values |
||||
* |
||||
* @see @ref GLFrameProfiler(Values, std::size_t), @ref setup() |
||||
*/ |
||||
typedef Containers::EnumSet<Value> Values; |
||||
|
||||
/**
|
||||
* @brief Default constructor |
||||
* |
||||
* Call @ref setup() to populate the profiler with measurements. |
||||
*/ |
||||
explicit GLFrameProfiler(); |
||||
|
||||
/**
|
||||
* @brief Constructor |
||||
* |
||||
* Equivalent to default-constructing an instance and calling |
||||
* @ref setup() afterwards. |
||||
*/ |
||||
explicit GLFrameProfiler(Values values, std::size_t maxFrameCount); |
||||
|
||||
/** @brief Copying is not allowed */ |
||||
GLFrameProfiler(const GLFrameProfiler&) = delete; |
||||
|
||||
/** @brief Move constructor */ |
||||
GLFrameProfiler(GLFrameProfiler&&) noexcept; |
||||
|
||||
/** @brief Copying is not allowed */ |
||||
GLFrameProfiler& operator=(const GLFrameProfiler&) = delete; |
||||
|
||||
/** @brief Move assignment */ |
||||
GLFrameProfiler& operator=(GLFrameProfiler&&) noexcept; |
||||
|
||||
~GLFrameProfiler(); |
||||
|
||||
/**
|
||||
* @brief Setup measured values |
||||
* @param values List of measuremed values |
||||
* @param maxFrameCount Max frame count over which to calculate a |
||||
* moving average. Expected to be at least @cpp 1 @ce. |
||||
* |
||||
* Calling @ref setup() on an already set up profiler will replace |
||||
* existing measurements with @p measurements and reset |
||||
* @ref measuredFrameCount() back to @cpp 0 @ce. |
||||
*/ |
||||
void setup(Values values, std::size_t maxFrameCount); |
||||
|
||||
/**
|
||||
* @brief Measured values |
||||
* |
||||
* Corresponds to the @p values parameter passed to |
||||
* @ref GLFrameProfiler(Values, std::size_t) or @ref setup(). |
||||
*/ |
||||
Values values() const; |
||||
|
||||
/**
|
||||
* @brief Whether given measurement is available |
||||
* |
||||
* Returns @cpp true @ce if enough frames was captured to calculate |
||||
* given @p value, @cpp false @ce otherwise. Expects that @p value was |
||||
* enabled. |
||||
*/ |
||||
bool isMeasurementAvailable(Value value) const; |
||||
|
||||
using FrameProfiler::isMeasurementAvailable; |
||||
|
||||
/**
|
||||
* @brief Mean frame time in nanoseconds |
||||
* |
||||
* Expects that @ref Value::FrameTime was enabled, and that measurement |
||||
* data is available. See the flag documentation for more information. |
||||
* @see @ref isMeasurementAvailable(), @ref measurementMean() |
||||
*/ |
||||
Double frameTimeMean() const; |
||||
|
||||
/**
|
||||
* @brief Mean CPU frame duration in nanoseconds |
||||
* |
||||
* Expects that @ref Value::CpuDuration was enabled, and that |
||||
* measurement data is available. See the flag documentation for more |
||||
* information. |
||||
* @see @ref isMeasurementAvailable(), @ref measurementMean() |
||||
*/ |
||||
Double cpuDurationMean() const; |
||||
|
||||
/**
|
||||
* @brief Mean GPU frame duration in nanoseconds |
||||
* |
||||
* Expects that @ref Value::GpuDuration was enabled, and that |
||||
* measurement data is available. See the flag documentation for more |
||||
* information. |
||||
* @see @ref isMeasurementAvailable(), @ref measurementMean() |
||||
*/ |
||||
Double gpuDurationMean() const; |
||||
|
||||
#ifndef MAGNUM_TARGET_GLES |
||||
/**
|
||||
* @brief Mean vertex fetch ratio in thousandths |
||||
* |
||||
* Expects that @ref Value::VertexFetchRatio was enabled, and that |
||||
* measurement data is available. See the flag documentation for more |
||||
* information. |
||||
* @requires_gl46 Extension @gl_extension{ARB,pipeline_statistics_query} |
||||
* @see @ref isMeasurementAvailable(), @ref measurementMean() |
||||
*/ |
||||
Double vertexFetchRatioMean() const; |
||||
|
||||
/**
|
||||
* @brief Mean primitive clip ratio in percentage thousandths |
||||
* |
||||
* Expects that @ref Value::PrimitiveClipRatio was enabled, and that |
||||
* measurement data is available. See the flag documentation for more |
||||
* information. |
||||
* @requires_gl46 Extension @gl_extension{ARB,pipeline_statistics_query} |
||||
* @see @ref isMeasurementAvailable(), @ref measurementMean() |
||||
*/ |
||||
Double primitiveClipRatioMean() const; |
||||
#endif |
||||
|
||||
private: |
||||
using FrameProfiler::setup; |
||||
|
||||
struct State; |
||||
Containers::Pointer<State> _state; |
||||
}; |
||||
|
||||
CORRADE_ENUMSET_OPERATORS(GLFrameProfiler::Values) |
||||
|
||||
/**
|
||||
@debugoperatorclassenum{GLFrameProfiler,GLFrameProfiler::Value} |
||||
@m_since_latest |
||||
*/ |
||||
MAGNUM_DEBUGTOOLS_EXPORT Debug& operator<<(Debug& debug, GLFrameProfiler::Value value); |
||||
|
||||
/**
|
||||
@debugoperatorclassenum{GLFrameProfiler,GLFrameProfiler::Values} |
||||
@m_since_latest |
||||
*/ |
||||
MAGNUM_DEBUGTOOLS_EXPORT Debug& operator<<(Debug& debug, GLFrameProfiler::Values value); |
||||
#endif |
||||
|
||||
}} |
||||
|
||||
#endif |
||||
@ -0,0 +1,242 @@
|
||||
/*
|
||||
This file is part of Magnum. |
||||
|
||||
Copyright © 2010, 2011, 2012, 2013, 2014, 2015, 2016, 2017, 2018, 2019 |
||||
Vladimír Vondruš <mosra@centrum.cz> |
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a |
||||
copy of this software and associated documentation files (the "Software"), |
||||
to deal in the Software without restriction, including without limitation |
||||
the rights to use, copy, modify, merge, publish, distribute, sublicense, |
||||
and/or sell copies of the Software, and to permit persons to whom the |
||||
Software is furnished to do so, subject to the following conditions: |
||||
|
||||
The above copyright notice and this permission notice shall be included |
||||
in all copies or substantial portions of the Software. |
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR |
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, |
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL |
||||
THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER |
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING |
||||
FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER |
||||
DEALINGS IN THE SOFTWARE. |
||||
*/ |
||||
|
||||
#include <Corrade/TestSuite/Compare/Numeric.h> |
||||
#include <Corrade/Utility/System.h> |
||||
|
||||
#include "Magnum/DebugTools/FrameProfiler.h" |
||||
#include "Magnum/GL/Context.h" |
||||
#include "Magnum/GL/Extensions.h" |
||||
#include "Magnum/GL/Framebuffer.h" |
||||
#include "Magnum/GL/Mesh.h" |
||||
#include "Magnum/GL/OpenGLTester.h" |
||||
#include "Magnum/GL/Renderbuffer.h" |
||||
#include "Magnum/GL/RenderbufferFormat.h" |
||||
#include "Magnum/MeshTools/Compile.h" |
||||
#include "Magnum/Primitives/Cube.h" |
||||
#include "Magnum/Shaders/Flat.h" |
||||
#include "Magnum/Trade/MeshData.h" |
||||
|
||||
namespace Magnum { namespace DebugTools { namespace Test { namespace { |
||||
|
||||
struct FrameProfilerGLTest: GL::OpenGLTester { |
||||
explicit FrameProfilerGLTest(); |
||||
|
||||
void test(); |
||||
#ifndef MAGNUM_TARGET_GLES |
||||
void vertexFetchRatioDivisionByZero(); |
||||
void primitiveClipRatioDivisionByZero(); |
||||
#endif |
||||
}; |
||||
|
||||
struct { |
||||
const char* name; |
||||
GLFrameProfiler::Values values; |
||||
} Data[]{ |
||||
{"gpu duration", GLFrameProfiler::Value::GpuDuration}, |
||||
{"cpu duration + gpu duration", GLFrameProfiler::Value::CpuDuration|GLFrameProfiler::Value::GpuDuration}, |
||||
{"frame time + gpu duration", GLFrameProfiler::Value::FrameTime|GLFrameProfiler::Value::GpuDuration}, |
||||
#ifndef MAGNUM_TARGET_GLES |
||||
{"gpu duration + vertex fetch ratio", GLFrameProfiler::Value::GpuDuration|GLFrameProfiler::Value::VertexFetchRatio}, |
||||
{"vertex fetch ratio + primitive clip ratio", GLFrameProfiler::Value::VertexFetchRatio|GLFrameProfiler::Value::PrimitiveClipRatio} |
||||
#endif |
||||
}; |
||||
|
||||
FrameProfilerGLTest::FrameProfilerGLTest() { |
||||
addInstancedTests({&FrameProfilerGLTest::test}, |
||||
Containers::arraySize(Data)); |
||||
|
||||
#ifndef MAGNUM_TARGET_GLES |
||||
addTests({&FrameProfilerGLTest::vertexFetchRatioDivisionByZero, |
||||
&FrameProfilerGLTest::primitiveClipRatioDivisionByZero}); |
||||
#endif |
||||
} |
||||
|
||||
void FrameProfilerGLTest::test() { |
||||
auto&& data = Data[testCaseInstanceId()]; |
||||
setTestCaseDescription(data.name); |
||||
|
||||
if(data.values & GLFrameProfiler::Value::GpuDuration) { |
||||
#ifndef MAGNUM_TARGET_GLES |
||||
if(!GL::Context::current().isExtensionSupported<GL::Extensions::ARB::timer_query>()) |
||||
CORRADE_SKIP(GL::Extensions::ARB::timer_query::string() + std::string(" is not available")); |
||||
#elif defined(MAGNUM_TARGET_WEBGL) && !defined(MAGNUM_TARGET_GLES2) |
||||
if(!GL::Context::current().isExtensionSupported<GL::Extensions::EXT::disjoint_timer_query_webgl2>()) |
||||
CORRADE_SKIP(GL::Extensions::EXT::disjoint_timer_query_webgl2::string() + std::string(" is not available")); |
||||
#else |
||||
if(!GL::Context::current().isExtensionSupported<GL::Extensions::EXT::disjoint_timer_query>()) |
||||
CORRADE_SKIP(GL::Extensions::EXT::disjoint_timer_query::string() + std::string(" is not available")); |
||||
#endif |
||||
} |
||||
|
||||
#ifndef MAGNUM_TARGET_GLES |
||||
if((data.values & GLFrameProfiler::Value::VertexFetchRatio) && !GL::Context::current().isExtensionSupported<GL::Extensions::ARB::pipeline_statistics_query>()) |
||||
CORRADE_SKIP(GL::Extensions::ARB::pipeline_statistics_query::string() + std::string(" is not available")); |
||||
#endif |
||||
|
||||
/* Bind some FB to avoid errors on contexts w/o default FB */ |
||||
GL::Renderbuffer color; |
||||
color.setStorage( |
||||
#if !(defined(MAGNUM_TARGET_WEBGL) && defined(MAGNUM_TARGET_GLES2)) |
||||
GL::RenderbufferFormat::RGBA8, |
||||
#else |
||||
GL::RenderbufferFormat::RGBA4, |
||||
#endif |
||||
Vector2i{32}); |
||||
GL::Framebuffer fb{{{}, Vector2i{32}}}; |
||||
fb.attachRenderbuffer(GL::Framebuffer::ColorAttachment{0}, color) |
||||
.bind(); |
||||
|
||||
Shaders::Flat3D shader; |
||||
GL::Mesh mesh = MeshTools::compile(Primitives::cubeSolid()); |
||||
|
||||
GLFrameProfiler profiler{data.values, 4}; |
||||
CORRADE_COMPARE(profiler.maxFrameCount(), 4); |
||||
|
||||
for(auto value: {GLFrameProfiler::Value::CpuDuration, |
||||
GLFrameProfiler::Value::GpuDuration, |
||||
#ifndef MAGNUM_TARGET_GLES |
||||
GLFrameProfiler::Value::VertexFetchRatio, |
||||
GLFrameProfiler::Value::PrimitiveClipRatio |
||||
#endif |
||||
}) |
||||
if(data.values & value) |
||||
CORRADE_VERIFY(!profiler.isMeasurementAvailable(value)); |
||||
|
||||
profiler.beginFrame(); |
||||
shader.draw(mesh); |
||||
Utility::System::sleep(1); |
||||
profiler.endFrame(); |
||||
|
||||
profiler.beginFrame(); |
||||
shader.draw(mesh); |
||||
profiler.endFrame(); |
||||
|
||||
Utility::System::sleep(10); |
||||
|
||||
profiler.beginFrame(); |
||||
shader.draw(mesh); |
||||
Utility::System::sleep(1); |
||||
profiler.endFrame(); |
||||
|
||||
profiler.beginFrame(); |
||||
shader.draw(mesh); |
||||
Utility::System::sleep(1); |
||||
profiler.endFrame(); |
||||
|
||||
MAGNUM_VERIFY_NO_GL_ERROR(); |
||||
|
||||
/* The GPU time should not be a total zero. Can't test upper bound because
|
||||
(especially on overloaded CIs) it all takes a magnitude more than |
||||
expected. */ |
||||
if(data.values & GLFrameProfiler::Value::GpuDuration) { |
||||
CORRADE_VERIFY(profiler.isMeasurementAvailable(GLFrameProfiler::Value::GpuDuration)); |
||||
CORRADE_COMPARE_AS(profiler.gpuDurationMean(), 100, |
||||
TestSuite::Compare::Greater); |
||||
} |
||||
|
||||
/* 3/4 frames took 1 ms, the ideal average is 0.75 ms. Can't test upper
|
||||
bound because (especially on overloaded CIs) it all takes a magnitude |
||||
more than expected. */ |
||||
if(data.values & GLFrameProfiler::Value::CpuDuration) { |
||||
CORRADE_VERIFY(profiler.isMeasurementAvailable(GLFrameProfiler::Value::CpuDuration)); |
||||
CORRADE_COMPARE_AS(profiler.cpuDurationMean(), 0.70*1000*1000, |
||||
TestSuite::Compare::GreaterOrEqual); |
||||
} |
||||
|
||||
#ifndef MAGNUM_TARGET_GLES |
||||
/* 24 unique vertices in 12 triangles, ideal ratio is 24/36 */ |
||||
if(data.values & GLFrameProfiler::Value::VertexFetchRatio) { |
||||
CORRADE_VERIFY(profiler.isMeasurementAvailable(GLFrameProfiler::Value::VertexFetchRatio)); |
||||
CORRADE_COMPARE_WITH(profiler.vertexFetchRatioMean()/1000, 0.6667, |
||||
TestSuite::Compare::around(0.1)); |
||||
} |
||||
|
||||
/* We use a default transformation, which means the whole cube should be
|
||||
visible, nothing clipped */ |
||||
if(data.values & GLFrameProfiler::Value::PrimitiveClipRatio) { |
||||
CORRADE_VERIFY(profiler.isMeasurementAvailable(GLFrameProfiler::Value::PrimitiveClipRatio)); |
||||
CORRADE_COMPARE(profiler.primitiveClipRatioMean()/1000, 0.0); |
||||
} |
||||
#endif |
||||
} |
||||
|
||||
#ifndef MAGNUM_TARGET_GLES |
||||
void FrameProfilerGLTest::vertexFetchRatioDivisionByZero() { |
||||
if(!GL::Context::current().isExtensionSupported<GL::Extensions::ARB::pipeline_statistics_query>()) |
||||
CORRADE_SKIP(GL::Extensions::ARB::pipeline_statistics_query::string() + std::string(" is not available")); |
||||
|
||||
GLFrameProfiler profiler{GLFrameProfiler::Value::VertexFetchRatio, 4}; |
||||
|
||||
profiler.beginFrame(); |
||||
profiler.endFrame(); |
||||
|
||||
profiler.beginFrame(); |
||||
profiler.endFrame(); |
||||
|
||||
profiler.beginFrame(); |
||||
profiler.endFrame(); |
||||
|
||||
profiler.beginFrame(); |
||||
profiler.endFrame(); |
||||
|
||||
MAGNUM_VERIFY_NO_GL_ERROR(); |
||||
|
||||
/* No draws happened, so the ratio should be 0 (and not crashing with a
|
||||
division by zero) */ |
||||
CORRADE_VERIFY(profiler.isMeasurementAvailable(GLFrameProfiler::Value::VertexFetchRatio)); |
||||
CORRADE_COMPARE(profiler.vertexFetchRatioMean(), 0.0); |
||||
} |
||||
|
||||
void FrameProfilerGLTest::primitiveClipRatioDivisionByZero() { |
||||
if(!GL::Context::current().isExtensionSupported<GL::Extensions::ARB::pipeline_statistics_query>()) |
||||
CORRADE_SKIP(GL::Extensions::ARB::pipeline_statistics_query::string() + std::string(" is not available")); |
||||
|
||||
GLFrameProfiler profiler{GLFrameProfiler::Value::PrimitiveClipRatio, 4}; |
||||
|
||||
profiler.beginFrame(); |
||||
profiler.endFrame(); |
||||
|
||||
profiler.beginFrame(); |
||||
profiler.endFrame(); |
||||
|
||||
profiler.beginFrame(); |
||||
profiler.endFrame(); |
||||
|
||||
profiler.beginFrame(); |
||||
profiler.endFrame(); |
||||
|
||||
MAGNUM_VERIFY_NO_GL_ERROR(); |
||||
|
||||
/* No draws happened, so the ratio should be 0 (and not crashing with a
|
||||
division by zero) */ |
||||
CORRADE_VERIFY(profiler.isMeasurementAvailable(GLFrameProfiler::Value::PrimitiveClipRatio)); |
||||
CORRADE_COMPARE(profiler.primitiveClipRatioMean(), 0.0); |
||||
} |
||||
#endif |
||||
|
||||
}}}} |
||||
|
||||
CORRADE_TEST_MAIN(Magnum::DebugTools::Test::FrameProfilerGLTest) |
||||
Loading…
Reference in new issue