From 2ee6bb3b043bc4d1140251b1ff8fbfc845cd29c3 Mon Sep 17 00:00:00 2001 From: Ichthyostega Date: Wed, 18 Mar 2026 23:23:06 +0100 Subject: [PATCH] OutputSlot: look for alternative ways to implement the proxy MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit After a lot of pondering over various solution schemes, it seems that `BufferProxyAdaptor` is too rigid for the task at hand. It could be retained as a technology demonstration, and used as blueprint for a custom-made implementation. In a nutshell, we need a `BufferStore` that directly delegates to the respective `Connection` — which also implies to use the `OutputSlot::AllocState` as an intermediate to hendle that delegation. --- src/vault/mem/buffer-proxy-adaptor.hpp | 15 +- src/vault/out/output-buffer-proxy.hpp | 173 +++++++++++++++++++++++ src/vault/out/output-slot-connection.hpp | 1 + wiki/renderengine.html | 43 ++++-- wiki/thinkPad.ichthyo.mm | 154 +++++++++++++++++++- 5 files changed, 360 insertions(+), 26 deletions(-) create mode 100644 src/vault/out/output-buffer-proxy.hpp diff --git a/src/vault/mem/buffer-proxy-adaptor.hpp b/src/vault/mem/buffer-proxy-adaptor.hpp index 131c42685..9584e035b 100644 --- a/src/vault/mem/buffer-proxy-adaptor.hpp +++ b/src/vault/mem/buffer-proxy-adaptor.hpp @@ -14,10 +14,14 @@ /** @file buffer-proxy-adaptor.hpp ** Adapter to expose a given memory block through a BuffHandle. ** This implementation building-block allows to integrate some specific data access - ** (e.g. related to input / output) through the buffer lifecycle protocol as defined by BufferProvider. - ** @todo BROKEN as of 12/2024 //////////////////////////////////////////////////////////////////////////////TICKET #1387 : can not properly compose BufferProvider - ** @see output-slot.hpp + ** through the buffer lifecycle protocol as defined by BufferProvider. + ** @remark 3/2026 this solution was developed as pathfinder to resolve a mismatch + ** between the »Buffer Provider Protocol« and the »Output Slot Protocol«. + ** Ultimately, however, it was not used; instead, a similar, specially + ** tailored construct was directly integrated. ** @see output-proxy-provider-test.cpp + ** @see output-buffer-proxy.hpp + ** @see output-slot.hpp */ #ifndef VAULT_MEM_BUFFER_PROXY_ADAPTOR_H @@ -46,7 +50,7 @@ namespace mem { /** - * Adapter to expose access controlled access to some memory resource + * Adapter to expose controlled access to some memory resource * through a [»Buffer Type«](\ref BuffDescr) and a BuffHandle as front-end, * in accordance with the BufferProvider protocol. * @@ -55,8 +59,7 @@ namespace mem { * - what a Buffer Type means * - is there only one resource or is this setup statefull? * - callback functors for the lifecycle stages related to the client's access - * @todo WIP-WIP 2/2026 used as prototype to forge a path ahead to resolve - * the structural problems with OutputSlot vs. BufferProvider ///////////////////////////////////////TICKET #1415 : prototyping to resolve structural mismatch between OutputSlot / DataSink / BufferProvider + * @todo 3/2026 this is prototyping code and was retained for demonstration purposes. */ template class BufferProxyAdaptor diff --git a/src/vault/out/output-buffer-proxy.hpp b/src/vault/out/output-buffer-proxy.hpp new file mode 100644 index 000000000..aff638871 --- /dev/null +++ b/src/vault/out/output-buffer-proxy.hpp @@ -0,0 +1,173 @@ +/* + OUTPUT-BUFFER-PROXY.hpp - Adaptor to access existing allocation via buffer handling protocol + + Copyright (C) + 2026 Hermann Vosseler + +  **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 output-buffer-proxy.hpp + ** Adapter to expose some buffer memory through a BuffHandle as front-end. + ** This implementation building-block allows to integrate the output buffers + ** managed by a specific output mechanism with the generic implementation of + ** OutputSlote, so that only a BuffHandle needs to be exposed to the client. + ** @see output-proxy-provider-test.cpp + ** @see output-buffer-proxy.hpp + ** @see output-slot.hpp + */ + +#ifndef VAULT_OUT_OUTPUT_BUFFER_PROXY_H +#define VAULT_OUT_OUTPUT_BUFFER_PROXY_H + + +#include "lib/error.hpp" +#include "lib/nocopy.hpp" +#include "lib/symbol.hpp" +#include "lib/meta/util.hpp" +#include "vault/mem/buffer-provider.hpp" +#include "vault/mem/buffer-provider-setup.hpp" +#include "vault/mem/heap-mem-buffer-store.hpp" +#include "vault/mem/simple-buffer-state-registry.hpp" + +#include +#include + + +namespace vault{ +namespace out { + + using std::move; + using lib::Literal; + using lib::HashVal; + using vault::mem::Buff; + using vault::mem::LocalTag; + using vault::mem::BuffAlloc; + using vault::mem::BuffDescr; + using vault::mem::BuffHandle; + using vault::mem::TypeHandler; ///////////////////////////OOO Rly? + using vault::mem::BufferProviderSetup; + + + + /** + * Adapter to expose controlled access to some memory resource + * through a [»Buffer Type«](\ref BuffDescr) and a BuffHandle as front-end, + * in accordance with the BufferProvider protocol. + * + * This template is an implementation building block and needs to be instantiated + * with a policy or configuration to define the flexible parts of the behaviour + * - what a Buffer Type means + * - is there only one resource or is this setup statefull? + * - callback functors for the lifecycle stages related to the client's access + * @todo 3/2026 this is prototyping code and was retained for demonstration purposes. + */ + template + class OutputBufferProxy + : util::NonCopyable + { + + class ProxyBufferStore + : public BufferProviderSetup::Store + , private CONF + { + + Buff* + asBuffer (LocalTag targetMarker) + { + void* buffMem{targetMarker}; + return static_cast (buffMem); + } + + uint + prepareBuffers (HashVal,uint,size_t) override + { + WARN (engine, "Announce invoked on a Proxy Buffer Provider; " + "Implies misuse as generic BufferProvider"); + return 1; + } + + BuffAlloc + provideBuffer (HashVal,size_t siz,LocalTag targetMarker) override + { + BuffAlloc storageSlot{asBuffer(targetMarker), siz, targetMarker}; + CONF::on_lock (storageSlot); + return storageSlot; + } + + void + mark_emitted (HashVal, BuffAlloc storageSlot) override + { + CONF::on_emit (storageSlot); + } + + void + detachBuffer (HashVal, BuffAlloc storageSlot) override + { + CONF::on_release (storageSlot); + } + + public: + ProxyBufferStore (CONF policy) + : CONF{move(policy)} + { } + }; + + struct Setup + : CONF + { + auto buildStage() { return std::make_unique("OutputBufferProxy"); } + auto buildStore() { return std::make_unique (move(*this)); } + }; // Note: possible since Setup does not use CONF + + class PassThroughBufferProvider + : public BufferProviderSetup + { + + public: + PassThroughBufferProvider (Setup setup) + : BufferProviderSetup{setup} + { } + + BuffDescr + registerBuffer (void* buff, size_t siz) + { + REQUIRE (siz); + REQUIRE (buff); + auto& typeKey = bufferStage_->defineBufferType (siz, TypeHandler::RAW, LocalTag(buff)); + return buildDescriptor (typeKey); + } + }; + + PassThroughBufferProvider proxyProvider_; + + + public: + OutputBufferProxy(CONF policy) + : proxyProvider_{Setup{move(policy)}} + { } + + template + BuffDescr + getDescriptorFor (TAR& dataBlock) + { + return proxyProvider_.registerBuffer(&dataBlock, sizeof(TAR)); + } + + template + BuffHandle + lockBuffer (TAR& dataBlock) + { + BuffDescr buffType = getDescriptorFor (dataBlock); + ENSURE (buffType.isValid()); + return buffType.lockBuffer(); + } + }; + + +}} // namespace vault::out +#endif /*VAULT_OUT_OUTPUT_BUFFER_PROXY_H*/ diff --git a/src/vault/out/output-slot-connection.hpp b/src/vault/out/output-slot-connection.hpp index a4d9220fd..8454602a5 100644 --- a/src/vault/out/output-slot-connection.hpp +++ b/src/vault/out/output-slot-connection.hpp @@ -33,6 +33,7 @@ #include "lib/error.hpp" #include "lib/nocopy.hpp" #include "vault/out/output-slot.hpp" +#include "vault/out/output-buffer-proxy.hpp" #include "lib/scoped-collection.hpp" #include "lib/iter-adapter-stl.hpp" #include "lib/iter-source.hpp" diff --git a/wiki/renderengine.html b/wiki/renderengine.html index 93b013cf3..bad0152c7 100644 --- a/wiki/renderengine.html +++ b/wiki/renderengine.html @@ -5214,11 +5214,11 @@ Thus there are two serious problem situations → OutputSlotImpl -
+
The OutputSlot interface describes a point where generated media data can actually be sent to the external world. It is expected to be implemented by adapters and bridges to existing drivers or external interface libraries, like a viewer widget in the GUI, ALSA or Jack sound output, rendering to file, using an external media format library. The design of this core facility was rather difficult and stretched out over quite some time span -- this page documents the considerations and the decisions taken.
 
 !intention
-OutpotSlot is a metaphor to unify the organisation of actual (system level) outputs; using this concept allows to separate and abstract the data calculation and the organisation of playback and rendering from the specifics of the actual output sink. Actual output possibilities (video in GUI window, video fullscreen, sound, Jack, rendering to file) can be added and removed dynamically from various components (Vault, Stage), all using the same resolution and mapping mechanisms (→ OutputManagement)
+OutputSlot is a metaphor to unify the organisation of actual (system level) outputs; using this concept allows to separate and abstract the data calculation and the organisation of playback and rendering from the specifics of the actual output sink. Actual output possibilities (video in GUI window, video fullscreen, sound, Jack, rendering to file) can be added and removed dynamically from various components (Vault, Stage), all using the same resolution and mapping mechanisms (→ OutputManagement)
 
 !design possibilities
 !!properties as a starting point
@@ -5240,16 +5240,16 @@ Data is handed over by the client invoking an {{{emit(time,...)}}} function on t
 :here the output mechanism owns the buffer. Within a certain time window prior to the expected time of the {{{emit()}}}-call, the client may obtain this buffer (pointer) to fill in the data. The slot implementation won't touch this buffer until the {{{emit()}}} handover, which in this case just provides the time and signalles the client is done with that buffer. If the data emitting handshake doesn't happen at all, it counts as late and superseded by the next handshake.
 
 !!relation to timing
-Besides the sink handles, allocation of an output slot defines some timing constraints, which are binding for the client. These include a grid of deadlines for each frame to deliver, plus a fixed //latency.// The output slot implementation itself is bound by external constraints to deliver data at a fixed framerate and aligned to an externally defined timing grid, plus the data needs to be handed over ahead of these time points by an time amount given by the latency. Depending on the data exchange model, there is an additional time window limiting the buffer management.
+Besides the sink handles, allocation of an output slot defines some timing constraints, to which the client must comply. These [[Timings]] are detailed and explicit, including a grid of deadlines for each frame to deliver, plus a fixed //latency.// Within this context, »latency« implies a requirement to be ahead of the nominal time by a certain amount, to compensate for the processing time that is necessary to propagate the media to the physical output pin. The output slot implementation itself is bound by external constraints to deliver data at a fixed framerate and aligned to an externally defined timing grid, and furthermore the data needs to be handed over ahead of these time points by an time amount given by the latency. Depending on the data exchange model, additional timing constraints may be imposed to further limit the time window when the buffer must be provided, transferred and released.
 
-The assumption is for the client to have elaborate timing capabilities at his disposal. More specifically, the client is assumed to be a job running within the engine scheduler and thus can be configured to run //after// another job has finished, and to run within certain time limits. Thus the client is able to provide a //current nominal time// -- which is suitably close to the actual wall clock time. The output slot implementation can be written such as to work out from this time specification if the call is timely or overdue -- and react accordingly.
+!!relation to buffer management
+{{red{WIP 11/11}}} meanwhile I have worked out the BufferProvider interface in detail, including an explicit buffer handover protocol that is is backed by a tiny state engine to track the BufferMetadata. This mechanism provides an additional transition to {{{BLOCKED}}} state when order or timing constraints are being violated. Yet how to detect and resolve such a floundered state from the engine point of view still remains to be addressed.
 
-{{red{TODO 6/11}}}in this spec, both data exchange models exhibit a weakness regarding the releasing of buffers. At which time is it safe to release a buffer, when the handover didn't happen? Do we need an explicit callback, and how could this callback be triggered? This is similar to the problem of closing a network connection, i.e. the problem is generally unsolvable, but can be handled pragmatically within certain limits.
-{{red{WIP 11/11}}}meanwhile I've worked out the BufferProvider interface in deail. There's now a deatiled buffer handover protocol defined, which is supported by a little state engine tracking BufferMetadata. This mechanism provides a transition to {{{BLOCKED}}} state when order or timing constraints are being violated, which practically solves this problem. How to detect and resolve such a floundered state from the engine point of view still remains to be addressed.
+{{red{WIP 3/2026}}} after a long break, we are now facing the problem how to make BufferProvider and OutputSlot work together. Both define a protocol and data exchange models -- and both exhibit a weakness regarding the releasing of buffers. At which time is it safe to release a buffer, when the handover didn't happen? This is similar to the problem of closing a network connection, i.e. the problem is generally unsolvable, but can be handled pragmatically within certain limits.
+* For handles in the Render Engine we can generally assume that the regular call path is always completed, which ensures that every handle gets released. Exceptions may happen, but are //not handled// -- rather the Engine will be shut down in such a case, which could be arranged to discard all buffer memory unconditionally
+* Regarding the »Output Slot Protocol« it is not clear yet which part will be responsible for checking the [[timing constraints|Timings]]; the actual {{{Connection}}} implementation looks like a plausible candidate. Furthermore, it is not clear yet {{red{3/26}}} where precisely an underlying buffer provider will be integrated, and if doing so is mandatory
 
-!!!Lifecycle and storage
-The concrete OutputSlot implementation is owned and managed by the facility actually providing the output possibility in question. For example, the GUI provides viewer widgets, while some sound output backend provides sound ports. The associated OutputSlot implementation object is required to stay alive as long as it's registered with some OutputManager. It needs to be de-registered explicitly prior to destruction -- and this deregistration may block until all clients using this slot did terminate. Beyond that, an output slot implementation is expected to handle all kinds of failures gracefully -- preferably just emitting a signal (callback functor).
-{{red{TODO 7/11: Deregistration is an unsolved problem....}}}
+At this point, the initial design draft from 2012 became bogged down in a thicket of confused implementation. Based on, and driven by a placeholder implementation at that time, a {{{BufferProvider}}} utilised to simplify building the test setup was conflated with the //interface// exposed towards prospective clients. Notably the prototype implementation relied on passing a BuffHandle from a backing BufferProvider directly to the client. Yet only the BuffHandle itself can be considered part of the protocol, and acts as client interface, necessitating a mechanism that propagates the //lifecycle events// (locking, emitting, releasing) into a state coordinator within the opened OutputSlot.
 
 -----
 !Implementation / design problems
@@ -5259,23 +5259,38 @@ How to handle the selection of those two data exchange models!
 Thus: Client gets an {{{OutputSlot&}}}, without knowing the exact implementation type ⇒ how can the client pick up the right strategy?
 Solving this problem through //generic programming// -- i.e coding both cases effectively different, but similar -- would require to provide both implementation options already at //compile time!//
 
-{{red{currently}}} I see two possible, yet quite different approaches...
+Generally speaking, I see two possible different approaches...
 ;generic
 :when creating individual jobs, we utilise a //factory obtained from the output slot.//
 ;unified
 :extend and adapt the protocol such to make both models similar; concentrate all differences //within a separate buffer provider.//
 !!!discussion
 the generic approach looks as it's becoming rather convoluted in practice. We'd need to hand over additional parameters to the factory, which passes them through to the actual job implementation created. And there would be a coupling between slot and job (the slot is aware it's going to be used by a job, and even provides the implementation). Obviously, a benefit is that the actual code path executed within the job is without indirections, and all written down in a single location. Another benefit is the possibility to extend this approach to cover further buffer handling models -- it doesn't pose any requirements on the structure of the buffer handling --
-On the other hand, if we accept to retrieve the buffer(s) via an indirection, which we kind of do anyway //within the render node implementation// -- the unified model looks more like a clean solution. It's more like doing away with some local optimisations possible if we handle the models explicitly, so it's not much of a loss, given that the majority of the processing time will be spent within the inner pixel calculation loops for frame processing anyway. When following this approach, the BufferProvider becomes a third, independent partner, and the slot cooperates tightly with this buffer provider, while the client (processing node) still just talks to the slot. Basically, this unified solution works like extending the shared buffer model to both cases.
+On the other hand, if we accept to retrieve the buffer(s) via an indirection, which we kind of do anyway //within the render node implementation// -- the unified model looks more like a clean solution. It's more like doing away with some local optimisations possible if we handle the models explicitly, so it's not much of a loss, given that the majority of the processing time will be spent within the inner pixel calculation loops for frame processing anyway. When following this approach, the BufferProvider becomes a third, independent partner, and the slot cooperates tightly with this buffer provider, while the client (processing node) still just talks to a front-end handle. Basically, this unified solution works like extending the shared buffer model to both cases. It is possible to go even one step further and dress up the output handling such that it looks similar to the normal buffer handling for the client.
 ⇒ __conclusion__: go for the unified approach!
 
 !!!unified data exchange cycle
-The planned delivery time of a frame is used as an ID throughout that cycle
-# within a defined time window prior to delivery, the client can ''allocate and retrieve the buffer'' from the BufferProvider.
+The planned delivery time of a frame, or (equivalently) the frame-number on a well defined time grid is used as an ID throughout that cycle
+# within a defined time window prior to delivery, the client can ''allocate and retrieve a working buffer'' in the form of a BuffHandle.
 # the client has to ''emit'' within a (short) time window prior to deadline
-# now the slot gets exclusive access to the buffer for output, signalling the buffer release to the buffer provider when done.
+# now the output mechanism behind the OutputSlot gets exclusive access to the buffer for output, signalling the buffer release when done.
 
 → OutputSlotImpl
+
+-----
+!!! Delegation and state managment
+In 2026, the prototype implementation had to be reworked, due to [[#1395|https://issues.lumiera.org/ticket/1395]].
+However, the general structure of a BufferProvider was elaborated to quite some degree, and seems plausible and roughly final. Thus it becomes difficult to integrate the very specific requirements imposed by output handling into an implementation framework concerned primarily with memory management.
+* the output format dictates the buffer layout; whenever an OutputSlot was allocated, there is only a single //buffer type//
+* the client requests a buffer //for a specific frame// (observing the given [[timing constraints|Timings]] however)
+* at that point, the actual connection implementation has to hand-out a buffer pointer
+* so we need to create metadata and track state transitions for a buffer that is managed elsewhere
+* furthermore, calls from the client through the BuffHandle, to signal {{{emit}}} and {{{release}}} must be routed to the proper connection.
+So the problem is that BufferProvider is conceived as a central service facility, while what we need here is a thin abstraction proxy. Fortunately, the {{{BufferProvider}}} implementation scheme offers some wiggle room, insofar a {{{LocalTag}}} can be stored with the metadata. So the task is to provide a suitable implememtation of {{{BufferProvider::BufferStore}}}, such that
+* the index number of the connection / data feed is embedded into a {{{LocalTag}}}
+* an actual output buffer, provided by the {{{Connection}}} back-end, is registered with the metadata and represented as BuffHandle
+* calls on that BuffHandle will invoke the {{{BufferStore}}} API and must be delegated to the corresponding {{{Connection}}}.
+Taken together, this implies that the {{{OutputSlot::AllocState<CON>}}} turns into a delegation hub. It has to maintain the active connections and can thus be used to back a custom made {{{BufferStore}}} implementation, using the information embedded into the {{{LocalTag}}} to select the proper {{{Connection}}} for delegation -- which provides the actual implementation of all buffer management and state handling for the specific output mechanism.
 
diff --git a/wiki/thinkPad.ichthyo.mm b/wiki/thinkPad.ichthyo.mm index c7503389e..8bb94cd39 100644 --- a/wiki/thinkPad.ichthyo.mm +++ b/wiki/thinkPad.ichthyo.mm @@ -114315,7 +114315,7 @@ StM_bind(Builder<R1> b1, Extension<R1,R2> extension) - + @@ -114327,9 +114327,26 @@ StM_bind(Builder<R1> b1, Extension<R1,R2> extension)

+ + + + + + +

+ Ich hab zu Beginn ein »Buffer Provider Protocol« konzipiert, auf Basis einer abstrakten Analyse der vorhersehbaren Verwendung. Das enthält bereits die Idee von Lifecycle-Phasen (und das folgt praktisch direkt aus first principles...). Dann habe ich, etwas später das »Output Slot Protocol« konzipiert, und mich dabei — mutmaßlich unbewußt — an die damit bereits abgesteckte, gedankliche Schablone gehalten. Dieses Design wurde viele Jahre nicht überprüft; erst jetzt, als die Integration der Engine als Ziel gesezt wurde, und dann die Architektur »gedreht« (Stichwort »Domain Ontology«), erst dann zeigte sich, daß beide Entwürfe im Prinzip kongruent sind, aber praktisch nicht zusammenpassen. In dieser Situation bin ich nicht im Stande, meine Designs einfach wegzuferfen — denn beide Designs beruhen auf einer Einsicht, die ich immer noch für zutreffend halte und nicht ungeschehen machen kann. +

+

+ Allerdings ist dadurch noch nicht die konkrete Form der BufferProvider-Implementierung festgelegt; im Besonderen nicht die Idee, wie ich mit den Metadaten umgehe, und daß wir eine Metadaten-Tabelle haben, die auch die Funktion der State-Engine mit übernimmt. Zwar könnte ich diese bestehende Implementierung „einfach“ wegwerfen — ich hab aber das glatte Gegenteil getan: ich hab in sie jetzt viel Zeit investiert. Was hat mich dazu motiviert? Es war nur die Intuition, daß die Idee mit den Metadaten in die richtige Richtung weist, und einen Spielraum öffnent; demgegenüber wäre eine Implementierung, die von konkreten Anforderungen getrieben wird, bereits komplett festgelegt. Der Preis dafür ist allerdings, daß ich mich damit auf das Design-Schema im BufferProvider festgelegt habe, und jetzt die Anfroderung, einen Buffer  lediglich durchzureichen, um dieses Schema herum einfädeln muß +

+ +
+ + +
@@ -114991,6 +115008,7 @@ StM_bind(Builder<R1> b1, Extension<R1,R2> extension)

+
@@ -116978,6 +116996,26 @@ StM_bind(Builder<R1> b1, Extension<R1,R2> extension)
+ + + + + + + + + + +

+ schlimmer noch, nach der jüngsten Überarbeitung sind auch die Datentypen aus der Metadaten-Verwaltung auf das Implementierungs-API gewandert (insofern das BufferStage-API nun dedizierte Lookups macht zu gegebenem typeKey) +

+ +
+
+ + + +
@@ -138022,18 +138060,122 @@ StM_bind(Builder<R1> b1, Extension<R1,R2> extension) - - - +

In der aktuellen Konfiguration hat jeder BufferProvider seine eigenes BufferStage-Implementierung, und darin eingebettet eine eigene Metadaten-Hashtable. Im Falle des BufferProxyAdaptors bedeutet das, für jede Allokation eines OutputSlot erneut eine Metadaten-Verwaltung zu konstruieren, die dann aber nur für wenige Buffer-Einträge genutzt wird. Da Output-Slots durchaus längerfristig leben, stellt das kein Performance-Problem da, aber könnte letztlich schon zu einigem Bloat führen

- -
+
+ + + + + + + +

+ und die Lebenszyklus-Callbacks müssen in diese Connection zurückfinden +

+ +
+
+ + + + +

+ da steckt die Buffer-Adresse drinnen, die dann konkret vom BufferStore auch wieder zurückgeliefert werden soll; denn dieser ist hier in dem speziellen Setup eigentlich nur ein »stub« und dient dazu einen bereits bestehenden und woanders verwalteten Buffer lediglich durchzureichen +

+ +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + +

+ Nur das LocalTag ist von Anfang bis Ende da +

+ +
+ + + + + + +

+ ...was heißt, im jeweiligen Aufruf ist bekannt um welchen Aufruf es sich handelt; sodann genügt der Index der Connection als Zusatz-Info, um das jeweils einschlägige Connection-API aufzurufen +

+ +
+
+ + + + + + +

+ das wären dann aber zwei Indirektions-Schritte (somit komplexer) und außerdem bräuchte es dann ein Implementierungs-API +

+ +
+
+ + + + + +
+ + + + + + + + + + + + + + + + + + + + + + +