It seems evident that the the problems with the existing BufferProvider implementation are rooted in a lack of understanding of the actual usage situation at that time(2011). This caused a »hexagonal design«, i.e. something entirely driven by uint testing for the sake of unit testing. So to identify the unsuitable decisions, some degree of deductive work is necessary, based on our much better understanding of the Render Engine. A crucial challenge is that buffer management must perform well within a massively concurrent environment, and do so without excessive latency.
267 lines
9.4 KiB
C++
267 lines
9.4 KiB
C++
/*
|
||
BufferProviderProtocol(Test) - demonstration of buffer provider usage cycle
|
||
|
||
Copyright (C)
|
||
2011, Hermann Vosseler <Ichthyostega@web.de>
|
||
|
||
**Lumiera** is free software; you can redistribute it and/or modify it
|
||
under the terms of the GNU General Public License as published by the
|
||
Free Software Foundation; either version 2 of the License, or (at your
|
||
option) any later version. See the file COPYING for further details.
|
||
|
||
* *****************************************************************/
|
||
|
||
/** @file buffer-provider-protocol-test.cpp
|
||
** unit test \ref BufferProviderProtocol_test
|
||
*/
|
||
|
||
|
||
#include "lib/error.hpp"
|
||
#include "lib/test/run.hpp"
|
||
#include "lib/test/test-helper.hpp"
|
||
#include "lib/test/tracking-dummy.hpp"
|
||
#include "lib/util-foreach.hpp"
|
||
#include "steam/engine/testframe.hpp"
|
||
#include "steam/engine/test-rand-ontology.hpp"
|
||
#include "steam/engine/diagnostic-buffer-provider.hpp"
|
||
#include "steam/engine/buffhandle-attach.hpp"
|
||
#include "steam/engine/bufftable.hpp"
|
||
|
||
using util::isSameObject;
|
||
using util::for_each;
|
||
|
||
|
||
namespace steam {
|
||
namespace engine{
|
||
namespace test {
|
||
|
||
using lib::test::Dummy;
|
||
|
||
using steam::engine::BuffHandle;
|
||
using LERR_(LIFECYCLE);
|
||
using LERR_(LOGIC);
|
||
|
||
|
||
namespace { // Test fixture
|
||
|
||
const uint TEST_ELMS = 20;
|
||
|
||
|
||
HashVal
|
||
do_some_computation (TestFrame const& src, TestFrame& work, int64_t* data)
|
||
{
|
||
REQUIRE (data);
|
||
ont::Param param = src.getChecksum();
|
||
ont::manipulateFrame (&work, &src, param);
|
||
for (uint i=0; i<TestFrame::BUFFSIZ; ++i)
|
||
*(data+i) = work.data()[i];
|
||
return work.getChecksum();
|
||
}
|
||
|
||
}
|
||
|
||
|
||
/**************************************************************************//**
|
||
* @test document the usage patterns of the »Buffer Provider Protocol«
|
||
* Notably this protocol is used by the Render Engine during recursive `pull()`
|
||
* calls to claim and release the working buffers for each step; furthermore,
|
||
* also the output buffers are packaged as `BuffHandle` and used through this
|
||
* protocol.
|
||
*
|
||
* However, the actual `BufferProvider` implementation used here is instrumented
|
||
* for diagnostics and does not actually _manage_ buffers; rather each buffer
|
||
* is allocated on the heap and never released, so that any allocation and
|
||
* all buffer contents can be verified after the fact.
|
||
*/
|
||
class BufferProviderProtocol_test : public Test
|
||
{
|
||
virtual void
|
||
run (Arg)
|
||
{
|
||
verifySimpleUsage();
|
||
verifyRenderingUsage();
|
||
verifyObjectAttachment();
|
||
verifyObjectAttachmentFailure();
|
||
}
|
||
|
||
|
||
void
|
||
verifySimpleUsage()
|
||
{
|
||
// Create Test fixture.
|
||
// In real usage, a suitable memory/frame/buffer provider
|
||
// will be preconfigured, depending on the usage context
|
||
BufferProvider& provider = DiagnosticBufferProvider::build();
|
||
|
||
BuffHandle buff = provider.lockBufferFor<TestFrame>();
|
||
CHECK (buff.isValid());
|
||
CHECK (sizeof(TestFrame) <= buff.size());
|
||
buff.accessAs<TestFrame>() = testData(0);
|
||
|
||
TestFrame& content = buff.accessAs<TestFrame>();
|
||
CHECK (testData(0) == content);
|
||
|
||
buff.emit();
|
||
buff.release();
|
||
CHECK (!buff.isValid());
|
||
VERIFY_ERROR (LIFECYCLE, buff.accessAs<TestFrame>() );
|
||
|
||
DiagnosticBufferProvider& checker = DiagnosticBufferProvider::access(provider);
|
||
CHECK (checker.buffer_was_used (0));
|
||
CHECK (checker.buffer_was_closed (0));
|
||
|
||
CHECK (testData(0) == checker.accessMemory (0));
|
||
}
|
||
|
||
|
||
/** @test demonstrate the full sequence of invocations during rendering
|
||
*/
|
||
void
|
||
verifyRenderingUsage()
|
||
{
|
||
// Test fixture: allows to track/verify allocations after the fact
|
||
BufferProvider& provider = DiagnosticBufferProvider::build();
|
||
|
||
using DataBuff = std::array<int64_t, TestFrame::BUFFSIZ>;
|
||
constexpr auto BUFFSIZ = sizeof(DataBuff);
|
||
|
||
BuffDescr desc1 = provider.getDescriptor<size_t>();
|
||
BuffDescr desc2 = provider.getDescriptor<TestFrame>(); // note: implies also sizeof(TestFrame)
|
||
BuffDescr desc3 = provider.getDescriptorFor(BUFFSIZ); // Can also request fixed sized raw storage.
|
||
// CHECK (desc1.verifyValidity()); /////////////////////////////////////////////////////////////////////////////OOO fails (which is wrong)
|
||
// CHECK (desc2.verifyValidity());
|
||
// CHECK (desc3.verifyValidity());
|
||
|
||
uint num1 = desc1.announce(TEST_ELMS);
|
||
uint num2 = desc1.announce(TEST_ELMS + 1);
|
||
uint num3 = desc2.announce(TEST_ELMS * 2);
|
||
CHECK (num1 == TEST_ELMS);
|
||
CHECK (num2 == TEST_ELMS + 1);
|
||
CHECK (num3 == TEST_ELMS * 2);
|
||
|
||
// emulate a progressive processing
|
||
BuffHandle workBuff = desc2.lockBuffer();
|
||
workBuff.accessAs<TestFrame>() = testData(0);
|
||
for (uint i=0; i<TEST_ELMS; ++i)
|
||
{
|
||
BuffHandle srcBuff = workBuff;
|
||
workBuff = desc2.lockBuffer();
|
||
|
||
BuffHandle markBuff = desc1.lockBuffer();
|
||
BuffHandle dataBuff = desc3.lockBuffer();
|
||
CHECK (srcBuff.isValid());
|
||
CHECK (markBuff.isValid());
|
||
CHECK (workBuff.isValid());
|
||
CHECK (dataBuff.isValid());
|
||
|
||
TestFrame& src = srcBuff.accessAs<TestFrame>();
|
||
size_t& mark = markBuff.accessAs<size_t>();
|
||
TestFrame& work = workBuff.accessAs<TestFrame>();
|
||
DataBuff& data = dataBuff.accessAs<DataBuff>();
|
||
|
||
mark = do_some_computation (src,work, &data[0]);
|
||
|
||
srcBuff.emit();
|
||
dataBuff.emit();
|
||
markBuff.emit();
|
||
|
||
srcBuff.release();
|
||
dataBuff.release();
|
||
markBuff.release();
|
||
// note : workBuff passed to next iteration
|
||
}
|
||
CHECK (workBuff.isValid());
|
||
auto endSum = workBuff.accessAs<TestFrame>().getChecksum();
|
||
workBuff.release(); // note: not every buffer need be emitted.
|
||
|
||
DiagnosticBufferProvider& checker = DiagnosticBufferProvider::access(provider);
|
||
CHECK (checker.buffer_was_used (0));
|
||
CHECK (checker.buffer_was_closed (0));
|
||
// CHECK (checker.all_buffers_released()); ///////////////OOO do we need this API?
|
||
}
|
||
|
||
|
||
void
|
||
verifyObjectAttachment()
|
||
{
|
||
BufferProvider& provider = DiagnosticBufferProvider::build();
|
||
BuffDescr type_A = provider.getDescriptorFor(sizeof(TestFrame));
|
||
BuffDescr type_B = provider.getDescriptorFor(sizeof(int));
|
||
BuffDescr type_C = provider.getDescriptor<int>();
|
||
|
||
BuffHandle handle_A = provider.lockBuffer(type_A);
|
||
BuffHandle handle_B = provider.lockBuffer(type_B);
|
||
BuffHandle handle_C = provider.lockBuffer(type_C);
|
||
|
||
CHECK (handle_A);
|
||
CHECK (handle_B);
|
||
CHECK (handle_C);
|
||
|
||
CHECK (sizeof(TestFrame) == handle_A.size());
|
||
CHECK (sizeof( int ) == handle_B.size());
|
||
CHECK (sizeof( int ) == handle_C.size());
|
||
|
||
TestFrame& embeddedFrame = handle_A.create<TestFrame>();
|
||
CHECK (isSameObject (*handle_A, embeddedFrame));
|
||
CHECK (embeddedFrame.isAlive());
|
||
CHECK (embeddedFrame.isSane());
|
||
|
||
VERIFY_ERROR (LOGIC, handle_B.create<TestFrame>()); // too small to hold a TestFrame
|
||
VERIFY_ERROR (LIFECYCLE, handle_C.create<int>()); // has already an attached TypeHandler (creating an int)
|
||
|
||
handle_A.release();
|
||
handle_B.release();
|
||
handle_C.release();
|
||
|
||
CHECK (embeddedFrame.isDead());
|
||
CHECK (embeddedFrame.isSane());
|
||
}
|
||
|
||
|
||
void
|
||
verifyObjectAttachmentFailure()
|
||
{
|
||
BufferProvider& provider = DiagnosticBufferProvider::build();
|
||
BuffDescr type_D = provider.getDescriptorFor(sizeof(Dummy));
|
||
|
||
Dummy::checksum() = 0;
|
||
BuffHandle handle_D = provider.lockBuffer(type_D);
|
||
CHECK (0 == Dummy::checksum()); // nothing created thus far
|
||
|
||
handle_D.create<Dummy>();
|
||
CHECK (0 < Dummy::checksum());
|
||
|
||
handle_D.release();
|
||
CHECK (0 == Dummy::checksum());
|
||
|
||
BuffHandle handle_DD = provider.lockBuffer(type_D);
|
||
|
||
CHECK (0 == Dummy::checksum());
|
||
Dummy::activateCtorFailure();
|
||
|
||
CHECK (handle_DD.isValid());
|
||
try
|
||
{
|
||
handle_DD.create<Dummy>();
|
||
NOTREACHED ("Dummy ctor should fail");
|
||
}
|
||
catch (int val)
|
||
{
|
||
CHECK (!handle_DD.isValid());
|
||
|
||
CHECK (0 < Dummy::checksum());
|
||
CHECK (val == Dummy::checksum());
|
||
}
|
||
|
||
VERIFY_ERROR (LIFECYCLE, handle_DD.accessAs<Dummy>() );
|
||
VERIFY_ERROR (LIFECYCLE, handle_DD.create<Dummy>() );
|
||
}
|
||
};
|
||
|
||
|
||
/** Register this test class... */
|
||
LAUNCHER (BufferProviderProtocol_test, "unit engine");
|
||
|
||
|
||
|
||
}}} // namespace steam::engine::test
|