LUMIERA.clone/src/steam/engine/weaving-pattern-builder.hpp
Ichthyostega 1f265044e5 Invocation: further rearrange and rework FeedManifold
What I'm about to do amounts to a massive generalisation, which is tricky.
Instead of having a fixed array-style layout, we want to accept arbitrary and mixed arguments.
Notably, we want to give the ''actual Library Plug-in'' a lot of leeway for binding:
- optionally, the library might want to require **Parameters** (which is the reason for this change)
- moreover, accepting input-buffers shall now be optional, since many generation functions do not need them
- and on top of all this, we want to accept an arbitrary mix of types for each kind.

So conceptually we are switching from C-style arrays to tuples with full type safety

''this going to become quite nasty and technical, I'm afraid...''
2024-12-15 19:02:04 +01:00

389 lines
16 KiB
C++
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/*
WEAVING-PATTERN-BUILDER.hpp - build an invocation pattern for media calculations
Copyright (C)
2024, 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 weaving-pattern-builder.hpp
** Construction kit to establish an invocation scheme for media calculations.
** Adapters and configuration is provided to invoke the actual _media processing function_
** in accordance to a fixed _wiring scheme:_
** - the function takes two arguments
** - these are an array of input and output buffer pointers
** - buffer sizes or types are assumed to be uniform over all »slots«
** - yet the input side my use another type than the output side
** @todo as of 10/2024, this scheme is established as prototype to explore how processing nodes
** can be build, connected and invoked; the expectation is however that this simple scheme
** is suitable to adapt and handle many common cases of invoking media processing functions,
** because the given _functor_ is constructed within a plug-in tailored to a specific
** media processing library (e.g. FFmpeg) and thus can be a lambda to forward to the
** actual function.
** @note steam::engine::Turnout mixes-in the steam::engine::MediaWeavingPattern, which in turn
** inherits from an *Invocation Adapter* given as template parameter. So this constitutes
** an *extension point* where other, more elaborate invocation schemes could be integrated.
**
**
** # Interplay of NodeBuider, PortBuilder and WeavingBuilder
**
** The steam::engine::WeavingBuilder defined here serves as the low-level builder and adapter
** to prepare the wiring and invocation. The builder-API allows to setup the wiring of input
** and output-»slots« and control some detail aspects like caching. However, without defining
** any connections explicitly, a simple 1:1 wiring scheme is employed
** - each _input slot_ of the function gets an input buffer, which is filled by _pulling_
** (i.e. invoking) a predecessor node (a so called »lead«).
** - for each _output slot_ a buffer is allocated for the processing function to drop off
** the calculated media data
** - only one of these output buffers is used as actual result, while the other buffers
** are just discarded (but may possibly be fed to the frame cache).
**
** Each [Processing Node](\ref ProcNode) represents one specific processing functionality on a
** logical level; yet such a node may be able to generate several „flavours“ of this processing,
** which are represented as *ports* on this node. Actually, each such port stands for one specific
** setup of a function invocation, with appropriate _wiring_ of input and output connections.
** For example, an audio filtering function may be exposed on port-#1 for stereo sound, while
** port-#2 may process the left, and port-#3 the right channel in isolation. It is entirely
** up to the library-adapter-plug-in what processing functions to expose, and in which flavours.
** The WeavingBuilder is used to generate a single \ref Turnout object, which corresponds to
** the invocation of a single port and thus one flavour of processing.
**
** At one architectural level above, the \ref NodeBuilder exposes the ability to set up a
** ProcNode, complete with several ports and connected to possibly several predecessor nodes.
** Using a sequence of NodeBuilder invocations, the _processing node graph_ can be built gradually,
** starting from the source (predecessors) and moving up to the _exit nodes,_ which produce the
** desired calculation results. The NodeBuilder offers a function to define the predecessor nodes
** (also designated as _lead nodes_), and it offers an [entrance point](\ref NodeBuilder::preparePort)
** to descend into a \ref PortBuilder, allowing to add the port definitions for this node step by step.
**
** On the implementation level, the PortBuilder inherits from the NodeBuilder and embeds a
** WeavingBuilder instance. Moreover, the actual parametrisations of the NodeBuilder template
** are chained to create a _functional data structure._ This intricate setup is necessary because
** the actual data structure of the node graph comprises several small descriptor arrays and
** interconnected pointers, which are all placed into consecutive chunks of memory, using a
** custom allocator, the AllocationCluster. The lib::Several is used as front-end to access
** these small collections of related objects, and the associated lib::SeveralBuilder provides
** the low-level memory allocation and object creation functionality. The purpose of this
** admittedly quite elaborate scheme is to generate a compact data structure, with high
** cache locality and without wasting too much memory. Since the exact number of elements
** and the size of those elements can be deduced only after the builder-API usage has
** been completed, the aforementioned functional datastructure is used to collect the
** parametrisation information for all ports, while delaying the actual object creation.
** With this technique, it is possible to generate all descriptors or entries of one
** kind in a single run, and placed optimally and compact into the memory allocation.
**
** @see turnout.hpp
** @see node-builder.hpp
** @see NodeLink_test
**
** @todo WIP-WIP-WIP as of 10/2024 prototyping how to build and invoke render nodes /////////////////////////TICKET #1371
**
*/
#ifndef STEAM_ENGINE_WEAVING_PATTERN_BUILDER_H
#define STEAM_ENGINE_WEAVING_PATTERN_BUILDER_H
//#include "steam/common.hpp"
#include "lib/error.hpp"
#include "lib/symbol.hpp"
//#include "steam/engine/channel-descriptor.hpp"
//#include "vault/gear/job.h"
#include "lib/several-builder.hpp"
#include "steam/engine/proc-id.hpp"
#include "steam/engine/engine-ctx.hpp"
#include "steam/engine/weaving-pattern.hpp"
#include "steam/engine/buffer-provider.hpp"
#include "steam/engine/buffhandle-attach.hpp" /////////////////OOO why do we need to include this? we need the accessAs<TY>() template function
#include "lib/test/test-helper.hpp" ////////////////////////////OOO TODO added for test
#include "lib/format-string.hpp"
//#include "lib/util-foreach.hpp"
//#include "lib/iter-adapter.hpp"
//#include "lib/meta/function.hpp"
//#include "lib/itertools.hpp"
#include "lib/util.hpp"
//#include <utility>
#include <functional>
//#include <array>
#include <vector>
#include <string>
namespace steam {
namespace engine {
namespace err = lumiera::error;
using StrView = std::string_view;
using std::forward;
// using lib::Literal;
using lib::Several;
using lib::Depend;
using util::_Fmt;
using util::max;
/**
* Typical base configuration for a Weaving-Pattern chain:
* - use a simple processing function
* - pass an input/output buffer array to this function
* - map all »slots« directly without any re-ordering
* - use a sufficiently sized FeedManifold as storage scheme
* @remark actual media handling plug-ins may choose to
* employ more elaborate _invocation adapters_
* specifically tailored to the library's needs.
*/
template<class FUN>
struct DirectFunctionInvocation
: util::MoveOnly
{
enum{ MAX_SIZ = _ProcFun<FUN>::MAXSZ };
using Manifold = FeedManifold<FUN>;
using Feed = SimpleFunctionInvocationAdapter<Manifold, FUN>;
std::function<Feed()> buildFeed;
/** when building the Turnout, prepare the _invocation adapter_
* @note processing function \a fun is bound by value into the closure,
* so that each invocation will create a copy of that function,
* embedded (and typically inlined) into the invocation adapter.
*/
DirectFunctionInvocation(FUN fun)
: buildFeed{[=]{ return Feed{fun}; }}
{ }
};
template<class POL, class I, class E=I>
using DataBuilder = lib::SeveralBuilder<I,E, POL::template Policy>;
template<uint siz>
using SizMark = std::integral_constant<uint,siz>;
/////////////////////////////////////////////////////////////////////////////////////////////////////////////TICKET #1371 : Prototyping: how to assemble a Turnout
/**
* Recursive functional data structure to collect weaving pattern data
* and finally to emplace a Turnout instance into the data storage
* for each port, as specified by preceding builder-API invocations.
* @tparam PAR recursive layering for preceding entries
* @tparam BUILD a builder functor to emplace one Turnout instance,
* thereby opaquely embedding all specific data typing.
* @tparam siz storage in bytes to hold data produced by \a BUILD
*/
template<class PAR, class BUILD, uint siz>
struct PatternData
: PAR
{
BUILD buildEntry;
uint size() { return 1 + PAR::size(); }
template<class DAB>
void
collectEntries (DAB& dataBuilder, uint cntElm =0, uint maxSiz =0)
{
PAR::collectEntries (dataBuilder, cntElm+1, max (siz,maxSiz));
buildEntry (dataBuilder);
}
PatternData(PAR&& predecessor, BUILD&& entryBuilder)
: PAR{move (predecessor)}
, buildEntry{move (entryBuilder)}
{ }
};
/**
* Data recursion end: prime the port data storage
* by reserving appropriate storage to hold all known Turnout elements.
*/
struct PatternDataAnchor
{
uint size() { return 0; }
template<class DAB>
void
collectEntries (DAB& dataBuilder, uint cntElm, uint maxSiz)
{
dataBuilder.reserve (cntElm, maxSiz);
}
};
template<class FUN>
using SimpleDirectInvoke = MediaWeavingPattern<DirectFunctionInvocation<FUN>>;
/**
* A low-level Builder to prepare and adapt for a specific node invocation.
* In this context, »weaving« refers to the way parameters and results of an
* processing function are provided, combined and forwarded within the setup
* for an actual Render Node invocation. When the invocation happens, a kind
* of preconfigured _blue print_ or invocation plan is executed; the purpose
* of the build at »Level-2« (≙the purpose of this code) is to preconfigure
* this invocation scheme. Using a _low level builder_ as controlled by the
* actual NodeBuilder and PortBuilder allows to introduce extension points
* and helps to abstract away internal technical details of the invocation.
* @tparam POL allocation and context configuration policy
* @tparam N maximum number of input and output slots
* @tparam FUN function or invocation adapter to invoke
*/
template<class POL, class FUN>
struct WeavingBuilder
: util::MoveOnly
{
using FunSpec = _ProcFun<FUN>;
using TurnoutWeaving = Turnout<SimpleDirectInvoke<FUN>>;
static constexpr SizMark<sizeof(TurnoutWeaving)> sizMark{};
static constexpr uint FAN_I = FunSpec::FAN_I;
static constexpr uint FAN_O = FunSpec::FAN_O;
using TypeMarker = std::function<BuffDescr(BufferProvider&)>;
using ProviderRef = std::reference_wrapper<BufferProvider>;
DataBuilder<POL, PortRef> leadPorts;
std::vector<TypeMarker> buffTypes;
std::vector<ProviderRef> providers;
uint resultSlot{0};
Depend<EngineCtx> ctx;
StrView nodeSymb_;
StrView portSpec_;
FUN fun_;
template<typename...INIT>
WeavingBuilder(FUN&& init, StrView nodeSymb, StrView portSpec, INIT&& ...alloInit)
: leadPorts{forward<INIT> (alloInit)...}
, nodeSymb_{nodeSymb}
, portSpec_{portSpec}
, fun_{move(init)}
{ }
WeavingBuilder&&
attachToLeadPort (ProcNode& lead, uint portNr)
{
if (leadPorts.size() >= FAN_I)
throw err::Logic{_Fmt{"Builder: attempt to add further input, "
"but all %d »input slots« of the processing function are already connected."}
% FAN_I
};
PortRef portRef{lead.getPort (portNr)};
leadPorts.append (portRef);
return move(*this);
}
template<class BU>
WeavingBuilder&&
appendBufferTypes (uint cnt)
{
if (buffTypes.size()+cnt > FAN_O)
throw err::Logic{_Fmt{"Builder: attempt add %d further output buffers, "
"while %d of %d possible outputs are already connected."}
% cnt % buffTypes.size() % FAN_O
};
while (cnt--)
buffTypes.emplace_back([](BufferProvider& provider)
{ return provider.getDescriptor<BU>(); });
ENSURE (buffTypes.size() <= FAN_O);
return move(*this);
}
WeavingBuilder&&
fillRemainingBufferTypes()
{
using BuffO = typename FunSpec::BuffO;
uint cnt = FAN_O - buffTypes.size();
return appendBufferTypes<BuffO>(cnt);
}
WeavingBuilder&&
connectRemainingInputs (DataBuilder<POL, ProcNodeRef>& knownLeads, uint defaultPort)
{
REQUIRE (leadPorts.size() <= FAN_I);
uint cnt = FAN_I - leadPorts.size();
if (FAN_I > knownLeads.size())
throw err::Logic{_Fmt{"Builder: attempt to auto-connect %d further »input slots«, "
"but this ProcNode has only %d predecessor nodes, while the "
"given processing function expects %d inputs."}
% cnt % knownLeads.size() % FAN_I
};
while (cnt--)
attachToLeadPort (knownLeads[leadPorts.size()], defaultPort);
return move(*this);
}
WeavingBuilder&&
selectResultSlot (uint idx)
{
this->resultSlot = idx;
return move(*this);
}
auto
build()
{
// discard excess storage prior to allocating the output types sequence
leadPorts.shrinkFit();
maybeFillDefaultProviders (buffTypes.size());
REQUIRE (providers.size() == buffTypes.size());
auto outTypes = DataBuilder<POL, BuffDescr>{leadPorts.policyConnect()}
.reserve (buffTypes.size());
uint i=0;
for (auto& typeConstructor : buffTypes)
outTypes.append (
typeConstructor (providers[i++]));
ENSURE (leadPorts.size() == FunSpec::FAN_I);
ENSURE (outTypes.size() == FunSpec::FAN_O);
using PortDataBuilder = DataBuilder<POL, Port>;
// provide a free-standing functor to build a suitable Port impl (≙Turnout)
return [leads = move(leadPorts.build())
,types = move(outTypes.build())
,procFun = move(fun_)
,resultIdx = resultSlot
,procID = ProcID::describe (nodeSymb_,portSpec_)
]
(PortDataBuilder& portData) mutable -> void
{
portData.template emplace<TurnoutWeaving> (procID
,move(leads)
,move(types)
,resultIdx
,move(procFun)
);
};
}
private:
void
maybeFillDefaultProviders (size_t maxSlots)
{
for (uint i=providers.size(); i < maxSlots; ++i)
providers.emplace_back (ctx().mem);
}
};
/////////////////////////////////////////////////////////////////////////////////////////////////////////////TICKET #1367 : (End)Prototyping: how to assemble a Turnout
}}// namespace steam::engine
#endif /*STEAM_ENGINE_WEAVING_PATTERN_BUILDER_H*/