diff --git a/.gitignore b/.gitignore index 4fe7b79..1d21075 100644 --- a/.gitignore +++ b/.gitignore @@ -14,3 +14,4 @@ configure cscope.out *.tmp .cache +.codex diff --git a/devices/avia0.dapss b/devices/avia0.dapss index fd20d25..d78bf20 100644 --- a/devices/avia0.dapss +++ b/devices/avia0.dapss @@ -1,6 +1,26 @@ #ifndef SMO_IP # define SMO_IP #endif + +edev|avia0|mesh()|livoxGen1()|livoxProto1(SMO_IP)|3JEDK380010Z39|| +edev|avia0|pcloudIntensity()|livoxGen1()|livoxProto1(SMO_IP)|3JEDK380010Z39|| -+edev|avia0|pcloudAmbience()|livoxGen1()|livoxProto1(SMO_IP)|3JEDK380010Z39 ++edev|avia0|pcloudAmbience()|livoxGen1()|livoxProto1(SMO_IP)|3JEDK380010Z39|| +/* Be positively disposed to dim ambience, but don't be so positively drawn that + * your eyelids droop and your pupils dilate, in human terms (i.e: no + * distraction). And no stupefaction from it either. + */ ++edev|avia0|postrin( + from-stimbuff=pcloudAmbience| + interest-pc=85| + passband-count-lt-val=8 + ) + |livoxGen1()|livoxProto1(SMO_IP)|3JEDK380010Z39|| +/* Be negatively disposed to high ambience, to the point of feeling un-ignorable + * pain when it's sufficiently high. + */ ++edev|avia0|negtrin( + from-stimbuff=pcloudAmbience| + interest-pc=85|distraction-pc=90|intolerable-pc=95| + passband-count-gt-val=120 + ) + |livoxGen1()|livoxProto1(SMO_IP)|3JEDK380010Z39 diff --git a/docs/design/intrin-thresholds.md b/docs/design/intrin-thresholds.md index 43c2c25..b2228db 100644 --- a/docs/design/intrin-thresholds.md +++ b/docs/design/intrin-thresholds.md @@ -9,6 +9,32 @@ allow the stimbuff API library to determine when to construct a stencil for postrin or negtrin data and present it to SMO with appropriate importance classifications. +**Intrinsic parameters may appear only on dedicated intrinsic qualeIfaceApi +specs.** The only supported forms are **`negtrin(...)`** and **`postrin(...)`**. +You cannot attach intrinsic threshold params to a sensory or other +non-intrinsic qualeIfaceApi (for example `pcloudIntensity`, `pcloudAmbience`, +`mesh`, `pcloud`, and so on). Those lines are for sensory streams only. + +A dedicated intrinsic line must name the sensory stimbuff it derives from via +**`from-stimbuff=`** (non-empty). Policy validation rejects +intrinsic params and passband-style comparators on sensory lines, and rejects +dedicated lines that omit `from-stimbuff`. + +## Dedicated `negtrin` / `postrin` qualeIfaceApi names + +On **`negtrin(...)`** or **`postrin(...)`**, the intrinsic family (negative vs +positive) is fixed by the API name. The **`negtrin-`** and **`postrin-`** +prefixes on parameter names are **optional**: you may use short forms +**`interest-...`**, **`distraction-...`**, **`stupefaction-` / `stupefying-...`**, +and **`intolerable-...`** with the unit suffix rules below. Prefixed names such +as **`negtrin-interest-pc`** remain valid when you prefer explicit spelling. + +Example (negtrin driven by ambience, short interest param, passband comparator): + +```` ++edev|avia0|negtrin(from-stimbuff=pcloudAmbience|interest-pc=85|passband-count-gt-val=120)|livoxGen1()|livoxProto1(SMO_IP)|3JEDK380010Z39 +```` + ## Unit Suffix Rules All intrinsic threshold params must include an explicit unit suffix. @@ -30,118 +56,74 @@ intrinsic classification. If multiple unit-suffixed variants targeting the same classification are supplied anyway, the lattermost supplied parameter takes precedence. -Shorthand params without a unit suffix are not permitted anymore. The -following forms are invalid because they all omit units: +Shorthand params without a unit suffix are not permitted. The following +forms are invalid because they omit units: - `[trin-]|intolerable>` -For stupefying and intolerable thresholds specifically, the `postrin-` -or `negtrin-` prefix may be omitted when the intended intrinsic is -already implied by the classification itself. Even in those cases, a -unit suffix is still mandatory. +On dedicated **`postrin(...)`** / **`negtrin(...)`** lines, the short forms +**`interest-...`**, **`distraction-...`**, etc. still require those unit +suffixes. ## Interest Threshold Parameters -**Parameter forms:** -- `postrin-interest-` -- `negtrin-interest-` +**Parameter forms (only inside `negtrin(...)` or `postrin(...)`):** + +- Prefixed: `postrin-interest-`, + `negtrin-interest-` +- Short (prefix optional because the line is already negtrin or postrin): + `interest-` **Description:** These parameters denote the value at which the stimbuff API library should construct a stencil for the postrin or negtrin that it delivers -for this DAPSpec's qualeIfaceApi's stimfeat, and present it to SMO via -postrinInd/negtrinInd with the "importance" argument set to "INTERESTING". +for this intrinsic spec, and present it to SMO via postrinInd/negtrinInd +with the "importance" argument set to "INTERESTING". -**Specification:** -- The parameter is specified as part of the `quale-iface-api-params` - in the DAP specification -- The unit suffix rules from the top of this document apply here too -- Separate thresholds can be specified for postrin and negtrin using the - respective prefixes - -**Example:** +**Example (dedicated negtrin line):** ```` -+idev|my-device|pcloudIntensity(postrin-interest-percentage=50|negtrin-interest-thr=30)|livoxGen1-pcloudIntensity()|livoxProto1()|3JEDK380010Z39 ++edev|avia0|negtrin(from-stimbuff=pcloudAmbience|interest-pc=85|passband-count-gt-val=120)|livoxGen1()|livoxProto1(SMO_IP)|3JEDK380010Z39 ```` -This example sets the interest threshold to 50% for postrins -(percentage-based) and 30 for negtrins (absolute threshold value). - ## Distraction Threshold Parameters -**Parameter forms:** -- `postrin-distraction-` -- `negtrin-distraction-` +**Parameter forms (only inside `negtrin(...)` or `postrin(...)`):** + +- Prefixed: `postrin-distraction-`, + `negtrin-distraction-` +- Short: `distraction-` (family is + fixed by the `negtrin` / `postrin` API name) **Description:** These parameters denote the value at which the stimbuff API library ought to construct a stencil and deliver it to SMO via negtrinInd/postrinInd with the importance argument set to "DISTRACTION". -**Specification:** -- The parameter is specified as part of the `quale-iface-api-params` - in the DAP specification -- The unit suffix rules from the top of this document apply here too -- Separate thresholds can be specified for postrin and negtrin using the - respective prefixes - -**Example:** -```` -+idev|my-device|pcloudIntensity(negtrin-distraction-thr=60|postrin-distraction-thr=40)|livoxGen1-pcloudIntensity()|livoxProto1()|3JEDK380010Z39 -```` - -This example sets the distraction threshold to 60 for negtrins and 40 -for postrins. - ## Stupefying Threshold Parameters (Postrin) -**Parameter forms:** -- `[postrin-]stupefaction-` -- `[postrin-]stupefying-` +**Parameter forms (only inside `postrin(...)`):** + +- Prefixed: `[postrin-]stupefaction-`, + `[postrin-]stupefying-` +- Short: `stupefaction-...`, `stupefying-...` with unit suffix **Description:** These parameters denote the value at which the stimbuff API library ought to construct a stencil and deliver it to SMO via postrinInd with the importance argument set to "STUPEFYING". -**Specification:** -- The parameter is specified as part of the `quale-iface-api-params` - in the DAP specification -- The unit suffix rules from the top of this document apply here too -- These parameters apply only to postrin data (positive intrinsic - stimuli) - -**Example:** -```` -+idev|my-device|pcloudIntensity(stupefying-thr=80)|livoxGen1-pcloudIntensity()|livoxProto1()|3JEDK380010Z39 -```` - -This example sets the stupefying threshold to 80 for postrins. - ## Intolerable Threshold Parameters (Negtrin) -**Parameter forms:** -- `[negtrin-]intolerable-` +**Parameter forms (only inside `negtrin(...)`):** + +- Prefixed: `[negtrin-]intolerable-` +- Short: `intolerable-` **Description:** These parameters denote the value at which the stimbuff API library ought to construct a stencil and deliver it to SMO via negtrinInd with the importance argument set to "INTOLERABLE". -**Specification:** -- The parameter is specified as part of the `quale-iface-api-params` - in the DAP specification -- The unit suffix rules from the top of this document apply here too -- These parameters apply only to negtrin data (negative intrinsic - stimuli) - -**Example:** -```` -+idev|my-device|pcloudIntensity(intolerable-thr=90)|livoxGen1-pcloudIntensity()|livoxProto1()|3JEDK380010Z39 -```` - -This example sets the intolerable threshold to 90 for negtrins. - ## Notes The way that SMO handles these importance levels is not yet fully worked diff --git a/docs/design/stencils.md b/docs/design/stencils.md index 3007cf6..610f7a9 100644 --- a/docs/design/stencils.md +++ b/docs/design/stencils.md @@ -18,9 +18,11 @@ descriptors tell it exactly which body spots are raising it. This direct information also assists SMO in narrowing down the scope of its DB searches for methods to handle (increase or decrease) the stimval. For -example, for a pcloudIntensity negtrin, we don't search DB by trying to -match all possible body spots with their stimvals. Rather, we search for all -the body spots that are described in the stencil. This optimizes DB searches +example, for a negtrin whose intrinsic pipeline is declared with a dedicated +`negtrin(...)` spec (and whose sensory data may come from intensity or another +stimbuff via `from-stimbuff`), we don't search DB by trying to match all +possible body spots with their stimvals. Rather, we search for all the body +spots that are described in the stencil. This optimizes DB searches and also makes negtrin relieving/postrin satisfying searches more explicit and obviously scoped items of knowledge. The eventual solution is automatically classified as being a method to relieve/satisfy intrins at @@ -60,9 +62,15 @@ be non-negotiable. ## n-stencils QualeIfaceApi Parameter -A new qualeIfaceApi parameter called `n-stencils` tells the stimbuff how -many stencils it can allocate and deliver to SMO simultaneously. The -stimbuff must wait until SMO returns stencils to it via postrinEventRdy or +Where a stim buff supports it, a qualeIfaceApi parameter called `n-stencils` +tells the stimbuff how many stencils it can allocate and deliver to SMO +simultaneously for **intrinsic** delivery. Intrinsic rate limiting and stencil +counts apply to **dedicated** intrinsic qualeIfaceApi specs (`negtrin(...)`, +`postrin(...)`) when the implementation attaches them—not to sensory-only +lines such as `pcloudIntensity` or `pcloudAmbience` (see +`docs/design/intrin-thresholds.md`). + +The stimbuff must wait until SMO returns stencils to it via postrinEventRdy or negtrinEventRdy before delivering new intrin events. Stimbuffs can deliver as many intrin events as they have stencils for. When all of their stencils have been given to SMO, they must wait until SMO returns a stencil before @@ -77,14 +85,17 @@ raising new intrins. - Stimbuffs must respect this limit and wait for stencil returns before allocating new ones -**Example (generic device):** +**Example (generic dedicated intrin line—shape only; names depend on device):** ``` -+idev|my-device|someQualeApi(n-stencils=4)|someStimBuffApi()|livoxProto1()|SERIAL ++idev|my-device|negtrin(from-stimbuff=someSensoryQuale|n-stencils=4)|someStimBuffApi()|livoxProto1()|SERIAL ``` -The `pcloudAmbience` Livox Gen1 path does **not** use the `n-stencils` parameter; ambience data is delivered as a dense float vector in the stimulus frame buffer, not via a separate stencil allocation list. +The Livox Gen1 **`pcloudAmbience`** sensory line does **not** use `n-stencils`; +ambience floats are delivered in the stimulus frame buffer. If Livox adds +`n-stencils` for intrinsic pipelines, it would appear on **`negtrin(...)`** / +**`postrin(...)`** lines (with `from-stimbuff`), not on `pcloudAmbience` itself. -**Deprecated example (do not use for Livox Gen1 ambience):** +**Invalid (sensory qualeIfaceApi must not carry intrin-oriented params):** ``` +idev|my-device|pcloudAmbience(n-stencils=4)|livoxGen1-pcloud()|livoxProto1()|3JEDK380010Z39 ``` diff --git a/docs/livox-gen1-lidar-dap-spec.md b/docs/livox-gen1-lidar-dap-spec.md index 3f7e81c..5c26849 100644 --- a/docs/livox-gen1-lidar-dap-spec.md +++ b/docs/livox-gen1-lidar-dap-spec.md @@ -41,21 +41,11 @@ Each stim-buff-api is designed to work with specific stim-iface libraries that u ``` **Stim-Buff-API**: `livoxGen1-pcloud` -**Quale-Iface-API**: `pcloudAmbience` - Delivers per-dagram average intensity floats; postrin/negtrin binding and passband-style aggregation are being revised (see intrinsic parameters below). +**Quale-Iface-API**: `pcloudAmbience` - Delivers per-dagram average intensity floats (sensory stream only). -**Intrinsic Stimuli Support** (for pcloudAmbience quale-iface-api): -The `pcloudAmbience` quale-iface-api is intended to export both a postrin and a negtrin whose -thresholds are configurable via standard quale-iface-api-params: -- **Postrin interest threshold**: Configurable via `postrin-interest-[percentage|pc|threshold|thresh|thr]` -- **Negtrin interest threshold**: Configurable via `negtrin-interest-[percentage|pc|threshold|thresh|thr]` - -The `-percentage` and `-pc` variants take percentages in the range -`0-100`. The `-threshold`, `-thresh`, and `-thr` variants take an -absolute number whose interpretation is defined by the API library -itself. Users should specify only one unit-suffixed variant per targeted -intrinsic threshold; if multiple such variants are supplied, the -lattermost one takes precedence. Shorthand forms without a unit suffix -are not valid. +Intrinsic thresholds, passband comparators, and `from-stimbuff` wiring are **not** configured on +`pcloudAmbience`. Use separate DAP lines with qualeIfaceApi **`negtrin(...)`** or **`postrin(...)`** +and `from-stimbuff=pcloudAmbience`; see `docs/design/intrin-thresholds.md` and the `PcloudAmbienceIntrinStimulusBuffer` path in the LivoxGen1 stim buff API. ### 3. Point Cloud Coordinate Data Device (Extrospector) @@ -166,6 +156,8 @@ The `livoxProto1` provider accepts the following parameters: |--------------|---------------|----------------|-------------| | Point Cloud Intensity | `livoxGen1-pcloudIntensity` | `pcloudIntensity` | Light intensity/reflectivity data | | Point Cloud Ambience | `livoxGen1-pcloud` | `pcloudAmbience` | Per-dagram average intensity vector (`float` × `n-dgrams-per-frame`) | +| Ambience-driven intrinsic (negtrin) | `livoxGen1-pcloud` | `negtrin` | Dedicated intrin pipeline; requires `from-stimbuff=pcloudAmbience` and intrin params per `docs/design/intrin-thresholds.md` | +| Ambience-driven intrinsic (postrin) | `livoxGen1-pcloud` | `postrin` | Same as negtrin row, for positive intrinsics | | Point Cloud Coordinates | `livoxGen1-pcloud` | `pcloud` | Spatial coordinate data | | Gyroscope | `livoxGen1-gyro` | `gyro` | Angular velocity measurements | | Accelerometer | `livoxGen1-accel` | `accel` | Linear acceleration measurements | diff --git a/include/user/intrinThresholdParams.h b/include/user/intrinThresholdParams.h index deed2fa..ca74605 100644 --- a/include/user/intrinThresholdParams.h +++ b/include/user/intrinThresholdParams.h @@ -49,6 +49,19 @@ inline constexpr std::array kNegIntThrParamNames = { "negtrin-interest-thr", }; +// Short interest-* names: only valid on dedicated postrin/negtrin qualeIfaceApi +// lines (docs/design/intrin-thresholds.md); never on sensory qualeIfaceApi specs. +inline constexpr std::array kIntrinInterestPcUnprefixed = { + "interest-percentage", + "interest-pc", +}; + +inline constexpr std::array kIntrinInterestThrUnprefixed = { + "interest-threshold", + "interest-thresh", + "interest-thr", +}; + inline constexpr std::array kPosDistPcParamNames = { "postrin-distraction-percentage", "postrin-distraction-pc", @@ -150,6 +163,88 @@ bool namesContain( return false; } +inline bool isDedicatedIntrinsQualeIfaceApi(std::string_view qualeIfaceApi) +{ + return qualeIfaceApi == "negtrin" || qualeIfaceApi == "postrin"; +} + +inline bool isKnownIntrinsPipelineParamName(std::string_view name) +{ + return namesContain(kPosIntPcParamNames, name) + || namesContain(kPosIntThrParamNames, name) + || namesContain(kNegIntPcParamNames, name) + || namesContain(kNegIntThrParamNames, name) + || namesContain(kIntrinInterestPcUnprefixed, name) + || namesContain(kIntrinInterestThrUnprefixed, name) + || namesContain(kPosDistPcParamNames, name) + || namesContain(kPosDistThrParamNames, name) + || namesContain(kNegDistPcParamNames, name) + || namesContain(kNegDistThrParamNames, name) + || namesContain(kStupefactionPcParamNames, name) + || namesContain(kStupefactionThrParamNames, name) + || namesContain(kIntolerablePcParamNames, name) + || namesContain(kIntolerableThrParamNames, name) + || name == "passband-count-gt-val" + || name == "passband-count-lt-val"; +} + +inline bool hasNonEmptyFromStimbuffParam( + const std::vector>& params) +{ + for (const auto& [key, value] : params) + { + if (key == "from-stimbuff" && !value.empty()) + { return true; } + } + + return false; +} + +/** + * Enforces dedicated-only intrin specs: intrinsic thresholds and passband + * comparators appear only on negtrin/postrin qualeIfaceApi specs with + * from-stimbuff. Embedding intrins in other qualeIfaceApi parameter lists is + * rejected (docs/design/intrin-thresholds.md). + */ +inline void validateIntrinsQualeApiPolicy( + const std::string& qualeIfaceApi, + const std::vector>& params) +{ + if (isDedicatedIntrinsQualeIfaceApi(qualeIfaceApi)) + { + if (!hasNonEmptyFromStimbuffParam(params)) + { + throw std::runtime_error( + "qualeIfaceApi '" + qualeIfaceApi + "' requires a non-empty " + "'from-stimbuff=' parameter naming the " + "sensory stimbuff that feeds this intrinsic pipeline."); + } + + return; + } + + for (const auto& [name, value] : params) + { + (void)value; + + if (isKnownIntrinsPipelineParamName(name)) + { + throw std::runtime_error( + "Intrinsic threshold and passband comparator params must not " + "appear on qualeIfaceApi '" + qualeIfaceApi + "'. Use dedicated " + "negtrin(...) or postrin(...) lines with from-stimbuff=... " + "(offending param: '" + name + "')."); + } + + if (name == "from-stimbuff") + { + throw std::runtime_error( + "'from-stimbuff' is only valid on negtrin(...) or postrin(...) " + "qualeIfaceApi specs."); + } + } +} + inline void validateNoForbiddenUnitlessIntrinParams( const std::vector>& params) { diff --git a/stimBuffApis/livoxGen1/livoxGen1.cpp b/stimBuffApis/livoxGen1/livoxGen1.cpp index 3313ef2..c051434 100644 --- a/stimBuffApis/livoxGen1/livoxGen1.cpp +++ b/stimBuffApis/livoxGen1/livoxGen1.cpp @@ -10,6 +10,7 @@ #include #include #include +#include #include #include #include @@ -513,6 +514,8 @@ static const StimBuffApiDesc livoxGen1ApiDesc = { {.name = "mesh"}, {.name = "pcloudIntensity"}, {.name = "pcloudAmbience"}, + {.name = "negtrin"}, + {.name = "postrin"}, {.name = "gyro"}, {.name = "accel"} }, @@ -645,12 +648,24 @@ extern "C" void livoxGen1_attachDeviceReq( return; } + try { + smo::intrin::validateIntrinsQualeApiPolicy( + desc->qualeIfaceApi, desc->qualeIfaceApiParams); + } + catch (const std::runtime_error& e) + { + std::cerr << __func__ << ": " << e.what() << std::endl; + cb.callbackFn(false, desc); + return; + } + if (!PcloudStimulusProducer::supportsQualeIfaceApi(qualeIfaceApi)) { // Unknown qualeIfaceApi std::cerr << __func__ << ": Unsupported qualeIfaceApi '" << qualeIfaceApi << "' for LivoxGen1. " - "Supported values: mesh, pcloudIntensity, pcloudAmbience" + "Supported values: mesh, pcloudIntensity, pcloudAmbience, " + "negtrin, postrin" << std::endl; cb.callbackFn(false, desc); return; diff --git a/stimBuffApis/livoxGen1/meshStimulusBuffer.h b/stimBuffApis/livoxGen1/meshStimulusBuffer.h index 81e5608..9f42b1b 100644 --- a/stimBuffApis/livoxGen1/meshStimulusBuffer.h +++ b/stimBuffApis/livoxGen1/meshStimulusBuffer.h @@ -4,6 +4,7 @@ #include #include #include +#include namespace smo { namespace stim_buff { @@ -13,6 +14,8 @@ class StimulusProducer; /** * MeshStimulusBuffer is a specialized StimulusBuffer for mesh data. + * Intrinsic threshold params are not allowed on mesh qualeIfaceApi lines; + * use dedicated negtrin/postrin specs (docs/design/intrin-thresholds.md). */ class MeshStimulusBuffer : public StimulusBuffer @@ -30,7 +33,11 @@ public: parent, deviceAttachmentSpec, histbuffMs, inputEngineConstraints, outputEngineConstraints, callbacks, flags) - {} + { + intrin::validateIntrinsQualeApiPolicy( + deviceAttachmentSpec->qualeIfaceApi, + deviceAttachmentSpec->qualeIfaceApiParams); + } ~MeshStimulusBuffer() = default; diff --git a/stimBuffApis/livoxGen1/pcloudAmbienceIntrinStimulusBuffer.h b/stimBuffApis/livoxGen1/pcloudAmbienceIntrinStimulusBuffer.h new file mode 100644 index 0000000..2f54026 --- /dev/null +++ b/stimBuffApis/livoxGen1/pcloudAmbienceIntrinStimulusBuffer.h @@ -0,0 +1,251 @@ +#ifndef _LIVOX_GEN1_PCLOUD_AMBIENCE_INTRIN_STIMULUS_BUFFER_H +#define _LIVOX_GEN1_PCLOUD_AMBIENCE_INTRIN_STIMULUS_BUFFER_H + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include "pcloudAmbienceQualeIfaceApi.h" + +namespace smo { +namespace stim_buff { + +enum class IntrinStatus +{ + DISABLED, + NEGTRIN, + POSTRIN, +}; + +struct ParsedAmbienceIntrinConfig +{ + IntrinStatus status; + uint32_t interestPercentage; + uint32_t interestThreshold; +}; + +inline bool isAmbienceIntrinEnabled(IntrinStatus intrinStatus) +{ + return intrinStatus != IntrinStatus::DISABLED; +} + +inline intrin::ParsedThresholdParam parseAmbienceThresholdParam( + const std::string& paramName, + const std::string& paramValue, + intrin::ThresholdUnit unit) +{ + try + { + return intrin::ParsedThresholdParam{ + .value = std::stoi(paramValue), + .unit = unit, + .matchedName = paramName, + .wasSpecified = true, + }; + } + catch (const std::exception& e) + { + throw std::runtime_error( + "Failed to parse '" + paramName + "' param value '" + paramValue + + "' as integer: " + e.what()); + } +} + +inline ParsedAmbienceIntrinConfig buildAmbienceIntrinConfig( + IntrinStatus status, + const intrin::ParsedThresholdParam& thresholdParam, + size_t nDgramsPerFrame) +{ + return ParsedAmbienceIntrinConfig{ + .status = status, + .interestPercentage = + thresholdParam.unit == intrin::ThresholdUnit::Percentage + ? static_cast(thresholdParam.value) + : 0U, + .interestThreshold = intrin::resolveThresholdValue( + thresholdParam, nDgramsPerFrame), + }; +} + +inline std::optional tryParseAmbienceIntrinConfig( + const std::string& paramName, + const std::string& paramValue, + size_t nDgramsPerFrame, + const std::string& qualeIfaceApi) +{ + using intrin::ThresholdUnit; + + const bool apiIsPostrin = (qualeIfaceApi == "postrin"); + const bool apiIsNegtrin = (qualeIfaceApi == "negtrin"); + + if (intrin::namesContain(intrin::kPosIntPcParamNames, paramName) + || (apiIsPostrin + && intrin::namesContain( + intrin::kIntrinInterestPcUnprefixed, paramName))) + { + return buildAmbienceIntrinConfig( + IntrinStatus::POSTRIN, + parseAmbienceThresholdParam( + paramName, paramValue, ThresholdUnit::Percentage), + nDgramsPerFrame); + } + + if (intrin::namesContain(intrin::kPosIntThrParamNames, paramName) + || (apiIsPostrin + && intrin::namesContain( + intrin::kIntrinInterestThrUnprefixed, paramName))) + { + return buildAmbienceIntrinConfig( + IntrinStatus::POSTRIN, + parseAmbienceThresholdParam( + paramName, paramValue, ThresholdUnit::Absolute), + nDgramsPerFrame); + } + + if (intrin::namesContain(intrin::kNegIntPcParamNames, paramName) + || (apiIsNegtrin + && intrin::namesContain( + intrin::kIntrinInterestPcUnprefixed, paramName))) + { + return buildAmbienceIntrinConfig( + IntrinStatus::NEGTRIN, + parseAmbienceThresholdParam( + paramName, paramValue, ThresholdUnit::Percentage), + nDgramsPerFrame); + } + + if (intrin::namesContain(intrin::kNegIntThrParamNames, paramName) + || (apiIsNegtrin + && intrin::namesContain( + intrin::kIntrinInterestThrUnprefixed, paramName))) + { + return buildAmbienceIntrinConfig( + IntrinStatus::NEGTRIN, + parseAmbienceThresholdParam( + paramName, paramValue, ThresholdUnit::Absolute), + nDgramsPerFrame); + } + + return std::nullopt; +} + +inline ParsedAmbienceIntrinConfig parseAmbienceIntrinConfig( + const std::shared_ptr& deviceAttachmentSpec, + size_t nDgramsPerFrame) +{ + const auto& params = deviceAttachmentSpec->qualeIfaceApiParams; + + for (auto paramIt = params.rbegin(); paramIt != params.rend(); ++paramIt) + { + const auto& [paramName, paramValue] = *paramIt; + const auto parsedConfig = tryParseAmbienceIntrinConfig( + paramName, + paramValue, + nDgramsPerFrame, + deviceAttachmentSpec->qualeIfaceApi); + if (parsedConfig.has_value()) + { return parsedConfig.value(); } + } + + return ParsedAmbienceIntrinConfig{ + .status = IntrinStatus::DISABLED, + .interestPercentage = 0U, + .interestThreshold = 0U, + }; +} + +inline void validateAmbienceIntrinComparatorConfig( + IntrinStatus intrinStatus, + const std::optional& passbandCountComparator) +{ + if (isAmbienceIntrinEnabled(intrinStatus) + && !passbandCountComparator.has_value()) + { + throw std::runtime_error( + "A PcloudAmbience intrinsic pipeline with an intrin threshold " + "must also specify either 'passband-count-gt-val' or " + "'passband-count-lt-val'"); + } +} + +class StimulusProducer; + +/** + * Intrinsic pipeline for LivoxGen1 ambience: attaches as qualeIfaceApi + * negtrin(...) or postrin(...) with from-stimbuff=pcloudAmbience. Parses + * interest thresholds and passband comparators; sensory data still flows + * through PcloudAmbienceStimulusBuffer. + */ +class PcloudAmbienceIntrinStimulusBuffer +: public StimulusBuffer +{ +public: + explicit PcloudAmbienceIntrinStimulusBuffer( + StimulusProducer& parent, + const std::shared_ptr& deviceAttachmentSpec, + int histbuffMs, + const StagingBuffer::IOEngineConstraints& inputEngineConstraints, + const StagingBuffer::IOEngineConstraints& outputEngineConstraints, + const SmoCallbacks& callbacks, + cl_mem_flags flags, + size_t nDgramsPerFrame_) + : StimulusBuffer( + parent, deviceAttachmentSpec, histbuffMs, + inputEngineConstraints, outputEngineConstraints, + callbacks, flags), + nDgramsPerFrame(nDgramsPerFrame_) + { + intrin::validateIntrinsQualeApiPolicy( + deviceAttachmentSpec->qualeIfaceApi, + deviceAttachmentSpec->qualeIfaceApiParams); + intrin::validateNoForbiddenUnitlessIntrinParams( + deviceAttachmentSpec->qualeIfaceApiParams); + + const auto intrinConfig = parseAmbienceIntrinConfig( + deviceAttachmentSpec, nDgramsPerFrame_); + intrinStatus = intrinConfig.status; + intrinInterestPercentage = intrinConfig.interestPercentage; + intrinInterestThreshold = intrinConfig.interestThreshold; + + passbandCountComparator = parseOptionalPcloudAmbienceParamComparator( + deviceAttachmentSpec); + validateAmbienceIntrinComparatorConfig( + intrinStatus, passbandCountComparator); + } + + ~PcloudAmbienceIntrinStimulusBuffer() = default; + + bool shouldTriggerIntrinEvent(uint32_t ambiencePassbandCount) const + { + if (!isAmbienceIntrinEnabled(intrinStatus)) + { return false; } + + return ambiencePassbandCount >= intrinInterestThreshold; + } + + PcloudAmbienceIntrinStimulusBuffer( + const PcloudAmbienceIntrinStimulusBuffer&) = delete; + PcloudAmbienceIntrinStimulusBuffer& operator=( + const PcloudAmbienceIntrinStimulusBuffer&) = delete; + PcloudAmbienceIntrinStimulusBuffer(PcloudAmbienceIntrinStimulusBuffer&&) = + delete; + PcloudAmbienceIntrinStimulusBuffer& operator=( + PcloudAmbienceIntrinStimulusBuffer&&) = delete; + +public: + IntrinStatus intrinStatus; + uint32_t intrinInterestPercentage; + uint32_t intrinInterestThreshold; + std::optional passbandCountComparator; + size_t nDgramsPerFrame; +}; + +} // namespace stim_buff +} // namespace smo + +#endif // _LIVOX_GEN1_PCLOUD_AMBIENCE_INTRIN_STIMULUS_BUFFER_H diff --git a/stimBuffApis/livoxGen1/pcloudAmbienceQualeIfaceApi.h b/stimBuffApis/livoxGen1/pcloudAmbienceQualeIfaceApi.h index d284ae1..70a1b2f 100644 --- a/stimBuffApis/livoxGen1/pcloudAmbienceQualeIfaceApi.h +++ b/stimBuffApis/livoxGen1/pcloudAmbienceQualeIfaceApi.h @@ -5,11 +5,34 @@ #include #include #include +#include #include +#include namespace smo { namespace stim_buff { +inline std::string parseRequiredFromStimbuffQualeIfaceName( + const std::vector>& params) +{ + for (auto it = params.rbegin(); it != params.rend(); ++it) + { + if (it->first != "from-stimbuff") + { continue; } + + if (it->second.empty()) + { + throw std::runtime_error( + "'from-stimbuff' must name a non-empty sensory qualeIfaceApi"); + } + + return it->second; + } + + throw std::runtime_error( + "internal: 'from-stimbuff' missing after intrin policy validation"); +} + enum ParamComparatorOp { OP_CMP_GT, @@ -21,14 +44,14 @@ struct ParamComparator ParamComparatorOp op; uint32_t value; - bool operator()(float ambienceVal) const + bool operator()(float passbandMetricVal) const { switch (op) { case OP_CMP_GT: - return ambienceVal > static_cast(value); + return passbandMetricVal > static_cast(value); case OP_CMP_LT: - return ambienceVal < static_cast(value); + return passbandMetricVal < static_cast(value); } throw std::runtime_error("Unsupported ParamComparatorOp"); @@ -41,15 +64,15 @@ inline std::optional parseOptionalPcloudAmbienceParamComparator const auto& params = deviceAttachmentSpec->qualeIfaceApiParams; constexpr int kParamNotSpecified = -1; const int gtVal = device::DeviceAttachmentSpec::parseOptionalParamAsInt( - params, "ambience-count-gt-val", kParamNotSpecified); + params, "passband-count-gt-val", kParamNotSpecified); const int ltVal = device::DeviceAttachmentSpec::parseOptionalParamAsInt( - params, "ambience-count-lt-val", kParamNotSpecified); + params, "passband-count-lt-val", kParamNotSpecified); if (gtVal != kParamNotSpecified && ltVal != kParamNotSpecified) { throw std::runtime_error( - "Only one of 'ambience-count-gt-val' or 'ambience-count-lt-val' " - "may be specified for a pcloudAmbience stim buff instance"); + "Only one of 'passband-count-gt-val' or 'passband-count-lt-val' " + "may be specified for a PcloudAmbience intrinsic pipeline"); } if (gtVal != kParamNotSpecified) diff --git a/stimBuffApis/livoxGen1/pcloudAmbienceStimulusBuffer.h b/stimBuffApis/livoxGen1/pcloudAmbienceStimulusBuffer.h index 0e99df7..3b0e347 100644 --- a/stimBuffApis/livoxGen1/pcloudAmbienceStimulusBuffer.h +++ b/stimBuffApis/livoxGen1/pcloudAmbienceStimulusBuffer.h @@ -2,164 +2,22 @@ #define _LIVOX_GEN1_PCLOUD_AMBIENCE_STIMULUS_BUFFER_H #include -#include #include -#include -#include -#include #include #include #include #include -#include "pcloudAmbienceQualeIfaceApi.h" namespace smo { namespace stim_buff { -enum class IntrinStatus -{ - DISABLED, - NEGTRIN, - POSTRIN, -}; - -struct ParsedAmbienceIntrinConfig -{ - IntrinStatus status; - uint32_t interestPercentage; - uint32_t interestThreshold; -}; - -inline bool isAmbienceIntrinEnabled(IntrinStatus intrinStatus) -{ - return intrinStatus != IntrinStatus::DISABLED; -} - -inline intrin::ParsedThresholdParam parseAmbienceThresholdParam( - const std::string& paramName, - const std::string& paramValue, - intrin::ThresholdUnit unit) -{ - try - { - return intrin::ParsedThresholdParam{ - .value = std::stoi(paramValue), - .unit = unit, - .matchedName = paramName, - .wasSpecified = true, - }; - } - catch (const std::exception& e) - { - throw std::runtime_error( - "Failed to parse '" + paramName + "' param value '" + paramValue - + "' as integer: " + e.what()); - } -} - -inline ParsedAmbienceIntrinConfig buildAmbienceIntrinConfig( - IntrinStatus status, - const intrin::ParsedThresholdParam& thresholdParam, - size_t nDgramsPerFrame) -{ - return ParsedAmbienceIntrinConfig{ - .status = status, - .interestPercentage = - thresholdParam.unit == intrin::ThresholdUnit::Percentage - ? static_cast(thresholdParam.value) - : 0U, - .interestThreshold = intrin::resolveThresholdValue( - thresholdParam, nDgramsPerFrame), - }; -} - -inline std::optional tryParseAmbienceIntrinConfig( - const std::string& paramName, - const std::string& paramValue, - size_t nDgramsPerFrame) -{ - using intrin::ThresholdUnit; - - if (intrin::namesContain(intrin::kPosIntPcParamNames, paramName)) - { - return buildAmbienceIntrinConfig( - IntrinStatus::POSTRIN, - parseAmbienceThresholdParam( - paramName, paramValue, ThresholdUnit::Percentage), - nDgramsPerFrame); - } - - if (intrin::namesContain(intrin::kPosIntThrParamNames, paramName)) - { - return buildAmbienceIntrinConfig( - IntrinStatus::POSTRIN, - parseAmbienceThresholdParam( - paramName, paramValue, ThresholdUnit::Absolute), - nDgramsPerFrame); - } - - if (intrin::namesContain(intrin::kNegIntPcParamNames, paramName)) - { - return buildAmbienceIntrinConfig( - IntrinStatus::NEGTRIN, - parseAmbienceThresholdParam( - paramName, paramValue, ThresholdUnit::Percentage), - nDgramsPerFrame); - } - - if (intrin::namesContain(intrin::kNegIntThrParamNames, paramName)) - { - return buildAmbienceIntrinConfig( - IntrinStatus::NEGTRIN, - parseAmbienceThresholdParam( - paramName, paramValue, ThresholdUnit::Absolute), - nDgramsPerFrame); - } - - return std::nullopt; -} - -inline ParsedAmbienceIntrinConfig parseAmbienceIntrinConfig( - const std::shared_ptr& deviceAttachmentSpec, - size_t nDgramsPerFrame) -{ - const auto& params = deviceAttachmentSpec->qualeIfaceApiParams; - - for (auto paramIt = params.rbegin(); paramIt != params.rend(); ++paramIt) - { - const auto& [paramName, paramValue] = *paramIt; - const auto parsedConfig = tryParseAmbienceIntrinConfig( - paramName, paramValue, nDgramsPerFrame); - if (parsedConfig.has_value()) - { return parsedConfig.value(); } - } - - return ParsedAmbienceIntrinConfig{ - .status = IntrinStatus::DISABLED, - .interestPercentage = 0U, - .interestThreshold = 0U, - }; -} - -inline void validateAmbienceIntrinComparatorConfig( - IntrinStatus intrinStatus, - const std::optional& ambienceCountComparator) -{ - if (isAmbienceIntrinEnabled(intrinStatus) - && !ambienceCountComparator.has_value()) - { - throw std::runtime_error( - "A pcloudAmbience stim buff instance with an intrin threshold " - "must also specify either 'ambience-count-gt-val' or " - "'ambience-count-lt-val'"); - } -} - -// Forward declaration class StimulusProducer; /** - * PcloudAmbienceStimulusBuffer is a specialized StimulusBuffer for ambience point cloud data. + * Sensory PcloudAmbience buffer: per-dgram ambience floats only. + * Intrinsic thresholds and passband comparators are only valid on dedicated + * negtrin/postrin qualeIfaceApi specs (PcloudAmbienceIntrinStimulusBuffer); + * see docs/design/intrin-thresholds.md. */ class PcloudAmbienceStimulusBuffer : public StimulusBuffer @@ -180,42 +38,21 @@ public: callbacks, flags), nDgramsPerFrame(nDgramsPerFrame_) { - intrin::validateNoForbiddenUnitlessIntrinParams( + intrin::validateIntrinsQualeApiPolicy( + deviceAttachmentSpec->qualeIfaceApi, deviceAttachmentSpec->qualeIfaceApiParams); - - const auto intrinConfig = parseAmbienceIntrinConfig( - deviceAttachmentSpec, nDgramsPerFrame_); - intrinStatus = intrinConfig.status; - intrinInterestPercentage = intrinConfig.interestPercentage; - intrinInterestThreshold = intrinConfig.interestThreshold; - - ambienceCountComparator = parseOptionalPcloudAmbienceParamComparator( - deviceAttachmentSpec); - validateAmbienceIntrinComparatorConfig( - intrinStatus, ambienceCountComparator); } ~PcloudAmbienceStimulusBuffer() = default; - bool shouldTriggerIntrinEvent(uint32_t ambiencePassbandCount) const - { - if (!isAmbienceIntrinEnabled(intrinStatus)) - { return false; } - - return ambiencePassbandCount >= intrinInterestThreshold; - } - - // Non-copyable, non-movable: inherited pinner lifetime is instance-bound PcloudAmbienceStimulusBuffer(const PcloudAmbienceStimulusBuffer&) = delete; - PcloudAmbienceStimulusBuffer& operator=(const PcloudAmbienceStimulusBuffer&) = delete; + PcloudAmbienceStimulusBuffer& operator=( + const PcloudAmbienceStimulusBuffer&) = delete; PcloudAmbienceStimulusBuffer(PcloudAmbienceStimulusBuffer&&) = delete; - PcloudAmbienceStimulusBuffer& operator=(PcloudAmbienceStimulusBuffer&&) = delete; + PcloudAmbienceStimulusBuffer& operator=( + PcloudAmbienceStimulusBuffer&&) = delete; public: - IntrinStatus intrinStatus; - uint32_t intrinInterestPercentage; - uint32_t intrinInterestThreshold; - std::optional ambienceCountComparator; size_t nDgramsPerFrame; }; diff --git a/stimBuffApis/livoxGen1/pcloudIntensityStimulusBuffer.h b/stimBuffApis/livoxGen1/pcloudIntensityStimulusBuffer.h index 2158bba..3bdae68 100644 --- a/stimBuffApis/livoxGen1/pcloudIntensityStimulusBuffer.h +++ b/stimBuffApis/livoxGen1/pcloudIntensityStimulusBuffer.h @@ -4,6 +4,7 @@ #include #include #include +#include namespace smo { namespace stim_buff { @@ -31,7 +32,11 @@ public: parent, deviceAttachmentSpec, histbuffMs, inputEngineConstraints, outputEngineConstraints, callbacks, flags) - {} + { + intrin::validateIntrinsQualeApiPolicy( + deviceAttachmentSpec->qualeIfaceApi, + deviceAttachmentSpec->qualeIfaceApiParams); + } ~PcloudIntensityStimulusBuffer() = default; diff --git a/stimBuffApis/livoxGen1/pcloudStimulusProducer.cpp b/stimBuffApis/livoxGen1/pcloudStimulusProducer.cpp index 415be2e..3f413b7 100644 --- a/stimBuffApis/livoxGen1/pcloudStimulusProducer.cpp +++ b/stimBuffApis/livoxGen1/pcloudStimulusProducer.cpp @@ -4,6 +4,7 @@ #include #include #include +#include #include #include #include @@ -13,6 +14,24 @@ #include "livoxGen1.h" #include "pcloudStimulusProducer.h" +namespace { + +void requirePcloudAmbienceFromStimbuff( + const std::shared_ptr& spec) +{ + const std::string fromName = + smo::stim_buff::parseRequiredFromStimbuffQualeIfaceName( + spec->qualeIfaceApiParams); + if (fromName != "pcloudAmbience") + { + throw std::runtime_error( + "LivoxGen1 PcloudAmbience intrinsic pipelines require " + "from-stimbuff=pcloudAmbience (got '" + fromName + "')."); + } +} + +} // namespace + namespace smo { namespace stim_buff { @@ -132,7 +151,8 @@ bool PcloudStimulusProducer::supportsQualeIfaceApi( const std::string& qualeIfaceApi) { return qualeIfaceApi == "mesh" || qualeIfaceApi == "pcloudIntensity" || - qualeIfaceApi == "pcloudAmbience"; + qualeIfaceApi == "pcloudAmbience" || qualeIfaceApi == "negtrin" || + qualeIfaceApi == "postrin"; } bool PcloudStimulusProducer::exportsQualeIfaceApi( @@ -228,6 +248,11 @@ PcloudStimulusProducer::getAttachedStimulusBuffer( if (std::dynamic_pointer_cast(buffer)) { return buffer; } } + else if (qualeIfaceApi == "negtrin" || qualeIfaceApi == "postrin") + { + if (std::dynamic_pointer_cast(buffer)) + { return buffer; } + } // Type mismatch - return nullptr return nullptr; @@ -259,6 +284,22 @@ void PcloudStimulusProducer::destroyAttachedStimulusBuffer( ambienceBuff.reset(); ambienceStimulusBuffer.store(nullptr, std::memory_order_release); } + auto negIntrinBuff = negtrinAmbienceIntrinStimulusBuffer.load( + std::memory_order_acquire); + if (negIntrinBuff == buffer) + { + negIntrinBuff.reset(); + negtrinAmbienceIntrinStimulusBuffer.store( + nullptr, std::memory_order_release); + } + auto posIntrinBuff = postrinAmbienceIntrinStimulusBuffer.load( + std::memory_order_acquire); + if (posIntrinBuff == buffer) + { + posIntrinBuff.reset(); + postrinAmbienceIntrinStimulusBuffer.store( + nullptr, std::memory_order_release); + } // Call base class implementation to remove from attachedStimulusBuffers StimulusProducer::destroyAttachedStimulusBuffer(buffer); @@ -343,12 +384,38 @@ PcloudStimulusProducer::getOrCreateAttachedStimulusBuffer( this->start(); return ambienceStimBuff; } + else if (qualeIfaceApi == "negtrin" || qualeIfaceApi == "postrin") + { + requirePcloudAmbienceFromStimbuff(deviceAttachmentSpec); + + auto intrinBuff = std::make_shared( + *this, deviceAttachmentSpec, histbuffMs, + openClAmbienceInputConstraints, openClAmbienceInputConstraints, + *smoHooksPtr, CL_MEM_READ_WRITE, + this->nDgramsPerStagingBufferFrame); + + this->stop(); + addAttachedStimulusBufferIfNotExists(intrinBuff); + if (qualeIfaceApi == "negtrin") + { + negtrinAmbienceIntrinStimulusBuffer.store( + intrinBuff, std::memory_order_release); + } + else + { + postrinAmbienceIntrinStimulusBuffer.store( + intrinBuff, std::memory_order_release); + } + this->start(); + return intrinBuff; + } else { throw std::runtime_error( "Unsupported qualeIfaceApi: '" + qualeIfaceApi + "' for " "PcloudStimulusProducer. " - "Supported values: mesh, pcloudIntensity, pcloudAmbience"); + "Supported values: mesh, pcloudIntensity, pcloudAmbience, " + "negtrin, postrin"); } } diff --git a/stimBuffApis/livoxGen1/pcloudStimulusProducer.h b/stimBuffApis/livoxGen1/pcloudStimulusProducer.h index fc55faf..74de08b 100644 --- a/stimBuffApis/livoxGen1/pcloudStimulusProducer.h +++ b/stimBuffApis/livoxGen1/pcloudStimulusProducer.h @@ -15,6 +15,7 @@ #include "meshStimulusBuffer.h" #include "pcloudIntensityStimulusBuffer.h" #include "pcloudAmbienceStimulusBuffer.h" +#include "pcloudAmbienceIntrinStimulusBuffer.h" namespace smo { namespace stim_buff { @@ -104,6 +105,10 @@ public: intensityStimulusBuffer; std::atomic> ambienceStimulusBuffer; + std::atomic> + negtrinAmbienceIntrinStimulusBuffer; + std::atomic> + postrinAmbienceIntrinStimulusBuffer; private: class ProduceFrameReq;