From eb751fcf2634a486e8ee127e7f44f3baaecaf7c9 Mon Sep 17 00:00:00 2001 From: Kiriti Gowda Date: Mon, 27 Jul 2026 10:54:05 -0700 Subject: [PATCH 01/19] OpenVX 1.3.2: implement pipelining/streaming/event extension Implements the OpenVX KHR pipelining extension (vx_khr_pipelining.h): - Graph scheduling modes (QUEUE_AUTO / QUEUE_MANUAL / streaming) - Graph-parameter enqueue/dequeue and done-reference tracking - Event registration/delivery (node completed, graph completed, graph-parameter consumed, user events) - Source/sink node pipeup pre-fill and node-state tracking - Replicated graph-parameter handling via object-array/pyramid siblings - Feature flag OPENVX_USE_PIPELINING, default ON Conformance results: - GraphPipeline.*: 109/109 pass - GraphStreaming.*: 24/24 pass --- amd_openvx/openvx/CMakeLists.txt | 9 + amd_openvx/openvx/ago/ago_interface.cpp | 155 ++++- amd_openvx/openvx/ago/ago_internal.h | 86 +++ amd_openvx/openvx/ago/ago_pipelining.cpp | 643 ++++++++++++++++++++ amd_openvx/openvx/ago/ago_platform.h | 5 + amd_openvx/openvx/ago/ago_util.cpp | 45 +- amd_openvx/openvx/api/vx_api.cpp | 127 +++- amd_openvx/openvx/api/vx_pipelining_api.cpp | 595 ++++++++++++++++++ 8 files changed, 1650 insertions(+), 15 deletions(-) create mode 100644 amd_openvx/openvx/ago/ago_pipelining.cpp create mode 100644 amd_openvx/openvx/api/vx_pipelining_api.cpp diff --git a/amd_openvx/openvx/CMakeLists.txt b/amd_openvx/openvx/CMakeLists.txt index 5c76524b9..6e47f9eb4 100644 --- a/amd_openvx/openvx/CMakeLists.txt +++ b/amd_openvx/openvx/CMakeLists.txt @@ -32,6 +32,13 @@ set(VERSION "1.3.2") project(openvx VERSION ${VERSION} LANGUAGES CXX) +option(OPENVX_USE_PIPELINING "Enable OpenVX Graph Pipelining/Streaming/Event extension (vx_khr_pipelining)" ON) +if(OPENVX_USE_PIPELINING) + add_definitions(-DOPENVX_USE_PIPELINING=1) +else() + add_definitions(-DOPENVX_USE_PIPELINING=0) +endif() + include_directories(include ago api) list(APPEND SOURCES @@ -63,6 +70,7 @@ list(APPEND SOURCES ago/ago_interface.cpp ago/ago_kernel_api.cpp ago/ago_kernel_list.cpp + ago/ago_pipelining.cpp ago/ago_platform.cpp ago/ago_util.cpp ago/ago_util_opencl.cpp @@ -70,6 +78,7 @@ list(APPEND SOURCES api/vxu.cpp api/vx_api.cpp api/vx_nodes.cpp + api/vx_pipelining_api.cpp ) add_library(openvx SHARED ${SOURCES}) diff --git a/amd_openvx/openvx/ago/ago_interface.cpp b/amd_openvx/openvx/ago/ago_interface.cpp index 2e5e88a21..6544f096a 100644 --- a/amd_openvx/openvx/ago/ago_interface.cpp +++ b/amd_openvx/openvx/ago/ago_interface.cpp @@ -1368,8 +1368,15 @@ vx_status agoVerifyNode(AgoNode * node) if (data) { if ((kernel->argConfig[arg] & (AGO_KERNEL_ARG_INPUT_FLAG | AGO_KERNEL_ARG_OUTPUT_FLAG)) == AGO_KERNEL_ARG_OUTPUT_FLAG) { vx_meta_format meta = &node->metaList[arg]; + // For user kernels without a validate callback, infer output meta from the + // bound object so source/sink kernels can verify. + if (!kernel->validate_f && data && kernel->argType[arg] && kernel->argType[arg] != VX_TYPE_REFERENCE) { + meta->data.ref.type = data->ref.type; + meta->data.u = data->u; + } if (kernel->argType[arg] && kernel->argType[arg] != VX_TYPE_REFERENCE && (meta->data.ref.type != kernel->argType[arg])) { agoAddLogEntry(&kernel->ref, VX_ERROR_INVALID_TYPE, "ERROR: agoVerifyGraph: kernel %s: output argument type mismatch for argument#%d\n", kernel->name, arg); + return VX_ERROR_INVALID_TYPE; } else if (meta->data.ref.type == VX_TYPE_IMAGE) { @@ -1520,6 +1527,7 @@ vx_status agoVerifyNode(AgoNode * node) // make sure that the data come from output validator matches with object if (data->u.arr.itemtype != meta->data.u.arr.itemtype) { agoAddLogEntry(&kernel->ref, VX_ERROR_INVALID_TYPE, "ERROR: agoVerifyGraph: kernel %s: invalid array type for argument#%d\n", kernel->name, arg); + return VX_ERROR_INVALID_TYPE; } else if (!data->u.arr.capacity || (meta->data.u.arr.capacity && meta->data.u.arr.capacity > data->u.arr.capacity)) { @@ -1552,6 +1560,7 @@ vx_status agoVerifyNode(AgoNode * node) // make sure that the data come from output validator matches with object if (data->u.objarr.itemtype != meta->data.u.objarr.itemtype) { agoAddLogEntry(&kernel->ref, VX_ERROR_INVALID_TYPE, "ERROR: agoVerifyGraph: kernel %s: invalid object-array type for argument#%d\n", kernel->name, arg); + return VX_ERROR_INVALID_TYPE; } else if (!data->u.objarr.numitems || (meta->data.u.objarr.numitems && meta->data.u.objarr.numitems > data->u.objarr.numitems)) { @@ -1572,6 +1581,7 @@ vx_status agoVerifyNode(AgoNode * node) // make sure that the data come from output validator matches with object if (data->u.scalar.type != meta->data.u.scalar.type) { agoAddLogEntry(&kernel->ref, VX_ERROR_INVALID_TYPE, "ERROR: agoVerifyGraph: kernel %s: invalid type for argument#%d\n", kernel->name, arg); + return VX_ERROR_INVALID_TYPE; } } @@ -1579,6 +1589,7 @@ vx_status agoVerifyNode(AgoNode * node) // make sure that the data come from output validator matches with object if ((data->u.mat.type != meta->data.u.mat.type) || (data->u.mat.columns != meta->data.u.mat.columns) || (data->u.mat.rows != meta->data.u.mat.rows)) { agoAddLogEntry(&kernel->ref, VX_ERROR_INVALID_TYPE, "ERROR: agoVerifyGraph: kernel %s: invalid matrix meta for argument#%d\n", kernel->name, arg); + return VX_ERROR_INVALID_TYPE; } } @@ -1588,6 +1599,7 @@ vx_status agoVerifyNode(AgoNode * node) else if (meta->data.ref.type == VX_TYPE_THRESHOLD) { if ((data->u.thr.thresh_type != meta->data.u.thr.thresh_type)) { agoAddLogEntry(&kernel->ref, VX_ERROR_INVALID_TYPE, "ERROR: agoVerifyGraph: kernel %s: invalid threshold meta for argument#%d\n", kernel->name, arg); + return VX_ERROR_INVALID_TYPE; } } @@ -1609,6 +1621,7 @@ vx_status agoVerifyNode(AgoNode * node) } if (mismatched) { agoAddLogEntry(&kernel->ref, VX_ERROR_INVALID_TYPE, "ERROR: agoVerifyGraph: kernel %s: invalid tensor meta for argument#%d\n", kernel->name, arg); + return VX_ERROR_INVALID_TYPE; } } @@ -1623,6 +1636,7 @@ vx_status agoVerifyNode(AgoNode * node) } else if (kernel->argType[arg]) { agoAddLogEntry(&kernel->ref, VX_ERROR_INVALID_TYPE, "ERROR: agoVerifyGraph: kernel %s: invalid type for argument#%d\n", kernel->name, arg); + return VX_ERROR_INVALID_TYPE; } } @@ -1696,7 +1710,6 @@ int agoVerifyGraph(AgoGraph * graph) graph->enable_node_level_gpu_flush = false; } #endif - return status; } @@ -1837,11 +1850,11 @@ vx_status agoComputeImageValidRectangleOutputs(AgoGraph * graph) AgoData * data = node->paramList[i]; if (data && param->direction == VX_OUTPUT) { if (data->ref.type == VX_TYPE_IMAGE) { - printf("valid_rect [ %5d %5d %5d %5d ] image %s\n", data->u.img.rect_valid.start_x, data->u.img.rect_valid.start_y, data->u.img.rect_valid.end_x, data->u.img.rect_valid.end_y, data->name.c_str()); + fprintf(stderr,"valid_rect [ %5d %5d %5d %5d ] image %s\n", data->u.img.rect_valid.start_x, data->u.img.rect_valid.start_y, data->u.img.rect_valid.end_x, data->u.img.rect_valid.end_y, data->name.c_str()); } else if (data->ref.type == VX_TYPE_PYRAMID) { for (vx_size level = 0; level < data->u.pyr.levels; level++) { - printf("valid_rect [ %5d %5d %5d %5d ] pyrL%d %s\n", data->children[level]->u.img.rect_valid.start_x, data->children[level]->u.img.rect_valid.start_y, data->children[level]->u.img.rect_valid.end_x, data->children[level]->u.img.rect_valid.end_y, (int)level, data->name.c_str()); + fprintf(stderr,"valid_rect [ %5d %5d %5d %5d ] pyrL%d %s\n", data->children[level]->u.img.rect_valid.start_x, data->children[level]->u.img.rect_valid.start_y, data->children[level]->u.img.rect_valid.end_x, data->children[level]->u.img.rect_valid.end_y, (int)level, data->name.c_str()); } } } @@ -2483,8 +2496,10 @@ int agoExecuteGraph(AgoGraph * graph) if (status) { if (status == VX_ERROR_GRAPH_ABANDONED) agoAddLogEntry((vx_reference)graph, VX_FAILURE, "INFO: kernel %s exec returned graph_stopped status: VX_ERROR_GRAPH_ABANDONED (%d)\n", kernel->name, status); - else + else { + agoNotifyNodeError(graph, node, status); agoAddLogEntry((vx_reference)graph, VX_FAILURE, "ERROR: kernel %s exec failed (%d:%s)\n", kernel->name, status, agoEnum2Name(status)); + } return status; } agoPerfCaptureStop(&node->perf); @@ -2525,6 +2540,7 @@ int agoExecuteGraph(AgoGraph * graph) return VX_ERROR_GRAPH_ABANDONED; } } + agoNotifyNodeCompleted(graph, node); } } } @@ -2552,6 +2568,11 @@ int agoExecuteGraph(AgoGraph * graph) } #endif } + // Notify completion for GPU nodes launched in this graph execution. + for (AgoNode * node = graph->nodeList.head; node; node = node->next) { + if (node->attr_affinity.device_type == AGO_KERNEL_FLAG_DEVICE_GPU) + agoNotifyNodeCompleted(graph, node); + } agoPerfProfileEntry(graph, ago_profile_type_wait_end, &graph->ref); graph->gpu_perf_total.kernel_enqueue += graph->gpu_perf.kernel_enqueue; graph->gpu_perf_total.kernel_wait += graph->gpu_perf.kernel_wait; @@ -2572,7 +2593,14 @@ int agoExecuteGraph(AgoGraph * graph) if (status == VX_SUCCESS) graph->state = VX_GRAPH_STATE_COMPLETED; - + // Advance streaming node state for each executed node. + for (AgoNode * node = graph->nodeList.head; node; node = node->next) { + node->node_exec_count++; + vx_uint32 threshold = node->pipeup_output_depth > 0 ? (node->pipeup_output_depth - 1) : 0; + if (node->node_exec_count >= threshold && node->node_state == VX_NODE_STATE_PIPEUP) { + node->node_state = VX_NODE_STATE_STEADY; + } + } return status; } @@ -2843,6 +2871,40 @@ vx_status agoGraphDumpPerformanceProfile(AgoGraph * graph, const char * fileName return VX_SUCCESS; } +static void agoPreFillSourceNodePipeup(AgoGraph * graph) +{ + for (AgoNode * node = graph->nodeList.head; node; node = node->next) { + vx_uint32 depth = node->pipeup_output_depth; + if (depth <= 1) + continue; + // source nodes have no input parameters + bool has_input = false; + for (vx_uint32 i = 0; i < node->paramCount; i++) { + if (node->parameters[i].direction == VX_INPUT) { + has_input = true; + break; + } + } + if (has_input) + continue; + // execute the node in pipeup state until one frame before steady + vx_uint32 target = depth > 0 ? (depth - 1) : 0; + + AgoKernel * kernel = node->akernel; + while (node->node_exec_count < target) { + vx_status s = VX_SUCCESS; + if (kernel && kernel->kernel_f) { + s = kernel->kernel_f(node, (vx_reference *)node->paramList, node->paramCount); + } + if (s != VX_SUCCESS) + break; + node->node_exec_count++; + } + if (node->node_exec_count >= target && node->node_state == VX_NODE_STATE_PIPEUP) + node->node_state = VX_NODE_STATE_STEADY; + } +} + int agoProcessGraph(AgoGraph * graph) { vx_status status = VX_ERROR_INVALID_REFERENCE; @@ -2855,7 +2917,24 @@ int agoProcessGraph(AgoGraph * graph) } // execute graph if possible if (status == VX_SUCCESS) { - if (graph->verified && graph->isReadyToExecute) { + if (graph->verified && graph->pipelining) { + AgoGraphPipeliningState * pipe = graph->pipelining; + if (pipe->schedule_mode == VX_GRAPH_SCHEDULE_MODE_QUEUE_MANUAL || + pipe->schedule_mode == VX_GRAPH_SCHEDULE_MODE_QUEUE_AUTO || + pipe->streaming_enabled) { + status = agoExecuteGraphPipelined(graph); + } + else if (graph->isReadyToExecute) { + status = agoExecuteGraph(graph); + } + else { + agoAddLogEntry(&graph->ref, VX_FAILURE, "ERROR: agoProcessGraph: not verified (%d) or not ready to execute (%d)\n", graph->verified, graph->isReadyToExecute); + status = VX_FAILURE; + } + } + else if (graph->verified && graph->isReadyToExecute) { + // For non-streaming execution, pre-fill source-node pipeup queues. + agoPreFillSourceNodePipeup(graph); status = agoExecuteGraph(graph); } else { @@ -2898,6 +2977,68 @@ int agoWaitGraph(AgoGraph * graph) vx_status status = VX_ERROR_INVALID_REFERENCE; if (agoIsValidGraph(graph)) { status = VX_SUCCESS; + if (graph->pipelining) { + AgoGraphPipeliningState * pipe = graph->pipelining; + // Streaming graphs are driven by the streaming thread; vxWaitGraph just + // needs to observe that no execution is active. + if (pipe->streaming_enabled) { + while (!pipe->streaming_stop.load()) { + std::this_thread::sleep_for(std::chrono::milliseconds(1)); + } + // wait for streaming thread to finish + if (pipe->streaming_thread.joinable()) + pipe->streaming_thread.join(); + return status; + } + // QUEUE_AUTO: stop the background executor, drain any refs that were + // enqueued but not yet processed, then restart the executor so future + // enqueues continue to be handled automatically. + if (pipe->schedule_mode == VX_GRAPH_SCHEDULE_MODE_QUEUE_AUTO) { + agoStopGraphPipelining(graph); + { + CAgoLock lock(graph->cs); + for (;;) { + bool any_ready = false; + for (auto& q : pipe->param_queues) { + if (q->enabled && !q->ready_refs.empty()) { + any_ready = true; + break; + } + } + if (!any_ready) + break; + int exec_status = agoExecutePipelinedGraphOnce(graph); + if (exec_status != VX_SUCCESS) { + status = exec_status; + break; + } + } + } + agoStartGraphPipeliningAutoExecutor(graph); + return status; + } + // QUEUE_MANUAL without a graph thread: drain synchronously. + if (!graph->hThread && pipe->schedule_mode == VX_GRAPH_SCHEDULE_MODE_QUEUE_MANUAL) { + CAgoLock lock(graph->cs); + for (;;) { + bool any_ready = false; + for (auto& q : pipe->param_queues) { + if (q->enabled && !q->ready_refs.empty()) { + any_ready = true; + break; + } + } + if (!any_ready) + break; + int exec_status = agoExecuteGraphPipelined(graph); + if (exec_status != VX_SUCCESS) { + status = exec_status; + break; + } + } + return status; + } + } graph->threadWaitCount++; if (graph->threadScheduleCount <= 0) // the graph was never scheduled so return VX_FAILURE return VX_FAILURE; @@ -2921,4 +3062,4 @@ int agoWaitGraph(AgoGraph * graph) status = graph->status; } return status; -} \ No newline at end of file +} diff --git a/amd_openvx/openvx/ago/ago_internal.h b/amd_openvx/openvx/ago/ago_internal.h index dce5b1332..3447302f0 100644 --- a/amd_openvx/openvx/ago/ago_internal.h +++ b/amd_openvx/openvx/ago/ago_internal.h @@ -546,6 +546,8 @@ struct AgoKernel { vx_uint32 gpu_buffer_update_param_index; vx_bool opencl_buffer_access_enable; vx_uint32 importing_module_index_plus1; + vx_uint32 pipeup_output_depth; + vx_uint32 pipeup_input_depth; public: AgoKernel(); ~AgoKernel(); @@ -616,6 +618,9 @@ struct AgoNode { vx_uint32 hierarchical_level; vx_status status; vx_perf_t perf; + vx_uint32 node_state; + vx_uint32 node_exec_count; + vx_uint32 pipeup_output_depth; vx_bool local_data_change_is_enabled; vx_bool local_data_set_by_implementation; struct { bool enable; int paramIndexScalar; int paramIndexArray; } gpu_scalar_array_output_sync; @@ -663,6 +668,70 @@ struct AgoNodeList { AgoNode * tail; AgoNode * trash; }; +struct AgoGraphParameterQueue { + std::mutex mtx; + std::condition_variable done_cv; + std::deque ready_refs; + std::deque consumed_refs; + std::deque done_refs; + std::vector valid_refs; + vx_uint32 index; + vx_uint32 max_depth; + bool enabled; +}; + +struct AgoGraphPipeliningState { + vx_enum schedule_mode; + vx_uint32 timeout_ms; + vx_uint32 event_timeout_ms; + vx_uint32 pipeline_depth; + bool streaming_enabled; + AgoNode * trigger_node; + std::atomic streaming_stop; + std::thread streaming_thread; + std::atomic active_executions; + std::mutex active_mtx; + std::condition_variable active_cv; + std::mutex execution_mtx; + std::thread executor_thread; + std::atomic executor_stop; + std::vector> param_queues; +public: + AgoGraphPipeliningState(); + ~AgoGraphPipeliningState(); +}; + +struct AgoEvent { + vx_enum event_type; + vx_uint64 timestamp; + vx_uint64 app_value; + AgoGraph * graph; + AgoNode * node; + vx_uint32 graph_parameter_index; + vx_status status; + void * user_parameter; +}; + +struct AgoEventRegistration { + vx_reference ref; + vx_enum event_type; + vx_uint32 app_value; + vx_uint32 graph_parameter_index; +}; + +struct AgoContextEventSystem { + std::mutex events_mtx; + std::condition_variable events_cv; + std::deque events; + std::mutex registrations_mtx; + std::vector registrations; + bool enabled; + vx_uint32 timeout_ms; +public: + AgoContextEventSystem(); + ~AgoContextEventSystem(); +}; + struct AgoGraph { AgoReference ref; std::string name; @@ -707,6 +776,7 @@ struct AgoGraph { bool enable_performance_profiling; std::vector performance_profile; std::map moduleHandle; + AgoGraphPipeliningState * pipelining; public: AgoGraph(); ~AgoGraph(); @@ -791,6 +861,7 @@ struct AgoContext { vx_size hip_mem_release_count; #endif AgoTargetAffinityInfo_ attr_affinity; + AgoContextEventSystem * events; public: AgoContext(); ~AgoContext(); @@ -907,6 +978,21 @@ void agoPerfCopyNormalize(AgoContext * context, vx_perf_t * perfDst, vx_perf_t * // log void agoRegisterLogCallback(vx_context context, vx_log_callback_f callback, vx_bool reentrant); void agoAddLogEntry(AgoReference * ref, vx_status status, const char *message, ...); +// pipelining +AgoGraphPipeliningState * agoGetGraphPipeliningState(AgoGraph * graph); +AgoContextEventSystem * agoGetContextEventSystem(AgoContext * context); +void agoStopGraphPipelining(AgoGraph * graph); +void agoStartGraphPipeliningAutoExecutor(AgoGraph * graph); +void agoStartGraphStreamingThread(AgoGraph * graph); +void agoPushEvent(AgoContext * context, const AgoEvent& evt); +int agoExecuteGraphPipelined(AgoGraph * graph); +int agoExecutePipelinedGraphOnce(AgoGraph * graph); +// event notifications +void agoNotifyGraphCompleted(AgoGraph * graph); +void agoNotifyNodeCompleted(AgoGraph * graph, AgoNode * node); +void agoNotifyNodeError(AgoGraph * graph, AgoNode * node, vx_status status); +void agoNotifyGraphParameterConsumed(AgoGraph * graph, vx_uint32 graph_parameter_index); +bool agoGraphHasNodeEventRegistrations(AgoGraph * graph); #if (ENABLE_OPENCL || ENABLE_HIP) int agoGpuOclAllocBuffers(AgoGraph * graph); diff --git a/amd_openvx/openvx/ago/ago_pipelining.cpp b/amd_openvx/openvx/ago/ago_pipelining.cpp new file mode 100644 index 000000000..40134fb81 --- /dev/null +++ b/amd_openvx/openvx/ago/ago_pipelining.cpp @@ -0,0 +1,643 @@ +/* +Copyright (c) 2015 - 2026 Advanced Micro Devices, Inc. All rights reserved. + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. +*/ + +#include "ago_internal.h" +#include + +// +// OpenVX Pipelining Extension - AGO internal helpers +// +// This file implements the core state management and execution helpers for +// the Khronos OpenVX pipelining/streaming/event-queue extension +// (vx_khr_pipelining.h). +// + +#if OPENVX_USE_PIPELINING + +AgoGraphPipeliningState * agoGetGraphPipeliningState(AgoGraph * graph) +{ + if (!graph) + return nullptr; + if (!graph->pipelining) { + graph->pipelining = new AgoGraphPipeliningState(); + } + return graph->pipelining; +} + +AgoContextEventSystem * agoGetContextEventSystem(AgoContext * context) +{ + if (!context) + return nullptr; + if (!context->events) { + context->events = new AgoContextEventSystem(); + } + return context->events; +} + +static void agoStopGraphPipeliningExecutor(AgoGraph * graph) +{ + AgoGraphPipeliningState * pipe = graph ? graph->pipelining : nullptr; + if (!pipe) + return; + + pipe->executor_stop.store(true); + if (pipe->executor_thread.joinable()) { + pipe->executor_thread.join(); + } + + pipe->streaming_stop.store(true); + if (pipe->streaming_thread.joinable()) { + pipe->streaming_thread.join(); + } +} + +void agoStopGraphPipelining(AgoGraph * graph) +{ + if (!graph) + return; + agoStopGraphPipeliningExecutor(graph); +} + +static vx_uint64 agoCurrentTimestampNs() +{ + auto now = std::chrono::steady_clock::now(); + auto ns = std::chrono::duration_cast(now.time_since_epoch()).count(); + return (vx_uint64)ns; +} + +bool agoGraphHasNodeEventRegistrations(AgoGraph * graph) +{ + if (!graph || !graph->ref.context) + return false; + AgoContextEventSystem * evsys = agoGetContextEventSystem(graph->ref.context); + if (!evsys) + return false; + std::lock_guard lock(evsys->registrations_mtx); + for (const auto& reg : evsys->registrations) { + if (reg.event_type == VX_EVENT_NODE_COMPLETED || reg.event_type == VX_EVENT_NODE_ERROR) { + AgoReference * r = (AgoReference *)reg.ref; + if (r && r->type == VX_TYPE_NODE && r->scope == (vx_reference)graph) + return true; + } + } + return false; +} + +static bool agoIsPipeliningGraph(AgoGraph * graph) +{ + AgoGraphPipeliningState * pipe = graph ? graph->pipelining : nullptr; + if (!pipe) + return false; + return pipe->schedule_mode != VX_GRAPH_SCHEDULE_MODE_NORMAL || pipe->streaming_enabled; +} + +static AgoGraphParameterQueue * agoGetGraphParameterQueue(AgoGraphPipeliningState * pipe, vx_uint32 index) +{ + if (!pipe || index >= pipe->param_queues.size()) + return nullptr; + return pipe->param_queues[index].get(); +} + +// + +static vx_uint32 agoFindEventAppValue(AgoContext * context, vx_reference ref, vx_enum event_type, vx_uint32 graph_parameter_index) +{ + AgoContextEventSystem * evsys = agoGetContextEventSystem(context); + if (!evsys) + return 0; + std::lock_guard lock(evsys->registrations_mtx); + for (const auto& reg : evsys->registrations) { + if (reg.ref == ref && reg.event_type == event_type && + (event_type != VX_EVENT_GRAPH_PARAMETER_CONSUMED || reg.graph_parameter_index == graph_parameter_index)) { + return reg.app_value; + } + } + return 0; +} + +static void agoRemoveEventRegistrations(AgoContext * context, vx_reference ref) +{ + AgoContextEventSystem * evsys = agoGetContextEventSystem(context); + if (!evsys) + return; + std::lock_guard lock(evsys->registrations_mtx); + auto& regs = evsys->registrations; + regs.erase(std::remove_if(regs.begin(), regs.end(), + [ref](const AgoEventRegistration& reg) { return reg.ref == ref; }), regs.end()); +} + +// Event helpers +// + +static void agoInternalPushEvent(AgoContext * context, const AgoEvent& evt) +{ + AgoContextEventSystem * evsys = agoGetContextEventSystem(context); + if (!evsys || !evsys->enabled) + return; + { + std::lock_guard lock(evsys->events_mtx); + evsys->events.push_back(evt); + } + evsys->events_cv.notify_one(); +} + +void agoPushEvent(AgoContext * context, const AgoEvent& evt) +{ + agoInternalPushEvent(context, evt); +} + +void agoNotifyGraphCompleted(AgoGraph * graph) +{ + if (!graph || !graph->ref.context) + return; + AgoContextEventSystem * evsys = agoGetContextEventSystem(graph->ref.context); + if (!evsys || !evsys->enabled) + return; + AgoEvent evt; + evt.event_type = VX_EVENT_GRAPH_COMPLETED; + evt.timestamp = agoCurrentTimestampNs(); + evt.app_value = agoFindEventAppValue(graph->ref.context, (vx_reference)graph, VX_EVENT_GRAPH_COMPLETED, 0); + evt.graph = graph; + evt.node = nullptr; + evt.graph_parameter_index = 0; + evt.status = VX_SUCCESS; + evt.user_parameter = nullptr; + agoInternalPushEvent(graph->ref.context, evt); +} + +void agoNotifyNodeCompleted(AgoGraph * graph, AgoNode * node) +{ + if (!graph || !node || !graph->ref.context) + return; + AgoContextEventSystem * evsys = agoGetContextEventSystem(graph->ref.context); + if (!evsys || !evsys->enabled) + return; + AgoEvent evt; + evt.event_type = VX_EVENT_NODE_COMPLETED; + evt.timestamp = agoCurrentTimestampNs(); + evt.app_value = agoFindEventAppValue(graph->ref.context, (vx_reference)node, VX_EVENT_NODE_COMPLETED, 0); + evt.graph = graph; + evt.node = node; + evt.graph_parameter_index = 0; + evt.status = VX_SUCCESS; + evt.user_parameter = nullptr; + agoInternalPushEvent(graph->ref.context, evt); +} + +void agoNotifyNodeError(AgoGraph * graph, AgoNode * node, vx_status status) +{ + if (!graph || !node || !graph->ref.context) + return; + AgoContextEventSystem * evsys = agoGetContextEventSystem(graph->ref.context); + if (!evsys || !evsys->enabled) + return; + AgoEvent evt; + evt.event_type = VX_EVENT_NODE_ERROR; + evt.timestamp = agoCurrentTimestampNs(); + evt.app_value = agoFindEventAppValue(graph->ref.context, (vx_reference)node, VX_EVENT_NODE_ERROR, 0); + evt.graph = graph; + evt.node = node; + evt.graph_parameter_index = 0; + evt.status = status; + evt.user_parameter = nullptr; + agoInternalPushEvent(graph->ref.context, evt); +} + +static void agoEmitRegisteredNodeEvents(AgoGraph * graph, vx_enum event_type, vx_status err_status) +{ + if (!graph || !graph->ref.context) + return; + AgoContextEventSystem * evsys = agoGetContextEventSystem(graph->ref.context); + if (!evsys || !evsys->enabled) + return; + std::lock_guard lock(evsys->registrations_mtx); + for (const auto& reg : evsys->registrations) { + if (reg.event_type != event_type) + continue; + AgoReference * r = (AgoReference *)reg.ref; + if (!r || r->type != VX_TYPE_NODE || r->scope != (vx_reference)graph) + continue; + AgoEvent evt; + evt.event_type = event_type; + evt.timestamp = agoCurrentTimestampNs(); + evt.app_value = reg.app_value; + evt.graph = graph; + evt.node = (AgoNode *)r; + evt.graph_parameter_index = 0; + evt.status = err_status; + evt.user_parameter = nullptr; + agoInternalPushEvent(graph->ref.context, evt); + } +} + +void agoNotifyGraphParameterConsumed(AgoGraph * graph, vx_uint32 graph_parameter_index) +{ + if (!graph || !graph->ref.context) + return; + AgoContextEventSystem * evsys = agoGetContextEventSystem(graph->ref.context); + if (!evsys || !evsys->enabled) + return; + AgoEvent evt; + evt.event_type = VX_EVENT_GRAPH_PARAMETER_CONSUMED; + evt.timestamp = agoCurrentTimestampNs(); + evt.app_value = agoFindEventAppValue(graph->ref.context, (vx_reference)graph, VX_EVENT_GRAPH_PARAMETER_CONSUMED, graph_parameter_index); + evt.graph = graph; + evt.node = nullptr; + evt.graph_parameter_index = graph_parameter_index; + evt.status = VX_SUCCESS; + evt.user_parameter = nullptr; + agoInternalPushEvent(graph->ref.context, evt); +} + +// +// Reference substitution for pipelined execution. +// After graph optimization the graph parameter may be attached to a wrapper +// node, while the actual work happens in internally created/rewired nodes. +// Rather than swapping only the graph-parameter node's paramList entries, we +// replace every occurrence of the default bound data object in the entire +// graph with the queued reference, execute, then swap back. +// + +struct AgoParamBinding { + AgoData * original; + AgoData * queued; +}; + +static void agoSwapDataRefInGraph(AgoGraph * graph, AgoData * dataFind, AgoData * dataReplace) +{ + if (dataFind == dataReplace) + return; + // Replace in all node parameter lists. + for (AgoNode * node = graph->nodeList.head; node; node = node->next) { + for (vx_uint32 i = 0; i < node->paramCount; i++) { + if (node->paramList[i] == dataFind) { + node->paramList[i] = dataReplace; + } + } + } + // Replace in supernode data lists (GPU path). +#if (ENABLE_OPENCL||ENABLE_HIP) + for (AgoSuperNode * super = graph->supernodeList; super; super = super->next) { + for (size_t i = 0; i < super->dataList.size(); i++) { + if (super->dataList[i] == dataFind) { + super->dataList[i] = dataReplace; + } + } + for (size_t i = 0; i < super->dataListForAgeDelay.size(); i++) { + if (super->dataListForAgeDelay[i] == dataFind) { + super->dataListForAgeDelay[i] = dataReplace; + } + } + } +#endif + // Replace ROI master links. + for (AgoData * adata = graph->dataList.head; adata; adata = adata->next) { + if (adata->ref.type == VX_TYPE_IMAGE && adata->u.img.isROI && adata->u.img.roiMasterImage == dataFind) { + adata->u.img.roiMasterImage = dataReplace; + } + } +} + +// Swap a graph parameter binding, expanding object-array/pyramid siblings when +// the queued reference belongs to a replicated object array/pyramid. +static void agoApplyDataRefSwapWithSiblings(AgoGraph * graph, AgoData * original, AgoData * queued) +{ + if (original == queued) + return; + AgoData * origParent = original ? original->parent : nullptr; + AgoData * queuedParent = queued ? queued->parent : nullptr; + if (origParent && queuedParent && origParent != queuedParent && + origParent->numChildren > 1 && + (origParent->ref.type == VX_TYPE_OBJECT_ARRAY || origParent->ref.type == VX_TYPE_PYRAMID) && + origParent->ref.type == queuedParent->ref.type && + origParent->numChildren == queuedParent->numChildren) { + for (vx_uint32 i = 0; i < (vx_uint32)origParent->numChildren; i++) { + agoSwapDataRefInGraph(graph, origParent->children[i], queuedParent->children[i]); + } + } else { + agoSwapDataRefInGraph(graph, original, queued); + } +} + +static std::vector agoCollectGraphParameterBindings(AgoGraph * graph) +{ + std::vector bindings; + bindings.resize(graph->parameters.size()); + for (vx_uint32 i = 0; i < (vx_uint32)graph->parameters.size(); i++) { + vx_parameter param = graph->parameters[i]; + if (!param || param->scope->type != VX_TYPE_NODE) { + bindings[i] = { nullptr, nullptr }; + continue; + } + AgoNode * node = (AgoNode *)param->scope; + if (!node) { + bindings[i] = { nullptr, nullptr }; + continue; + } + AgoData * original = (param->index < node->paramCount) ? node->paramList[param->index] : nullptr; + bindings[i] = { original, nullptr }; + } + return bindings; +} + +static void agoApplyQueuedRefsToBindings(AgoGraph * graph, + AgoGraphPipeliningState * pipe, + std::vector& bindings, + std::vector& consumed_refs) +{ + consumed_refs.assign(bindings.size(), nullptr); + for (size_t i = 0; i < bindings.size(); i++) { + AgoGraphParameterQueue * q = agoGetGraphParameterQueue(pipe, (vx_uint32)i); + + if (!q || q->ready_refs.empty()) { + continue; + } + AgoData * ref = q->ready_refs.front(); + q->ready_refs.pop_front(); + q->consumed_refs.push_back(ref); + consumed_refs[i] = ref; + if (ref) { + agoRetainData(graph, ref, false); + } + bindings[i].queued = ref; + if (bindings[i].original) { + agoApplyDataRefSwapWithSiblings(graph, bindings[i].original, ref); + } + } +} + +static void agoRestoreBindings(AgoGraph * graph, std::vector& bindings) +{ + for (auto& b : bindings) { + if (b.original && b.queued) { + agoApplyDataRefSwapWithSiblings(graph, b.queued, b.original); + } + } +} + +static void agoMoveConsumedRefsToDone(AgoGraph * graph) +{ + AgoGraphPipeliningState * pipe = graph->pipelining; + if (!pipe) + return; + for (auto& q : pipe->param_queues) { + std::lock_guard lock(q->mtx); + + while (!q->consumed_refs.empty()) { + q->done_refs.push_back(q->consumed_refs.front()); + q->consumed_refs.pop_front(); + } + if (!q->done_refs.empty()) { + // Notify that a reference at this parameter was consumed during this execution. + agoNotifyGraphParameterConsumed(graph, q->index); + } + } + for (auto& q : pipe->param_queues) { + q->done_cv.notify_all(); + } +} + +// +// Single pipelined execution instance (pipeline depth = 1 serialized path). +// +int agoExecutePipelinedGraphOnce(AgoGraph * graph) +{ + AgoGraphPipeliningState * pipe = graph->pipelining; + if (!pipe) + return VX_FAILURE; + + // Collect default data references bound to each graph parameter. + std::vector bindings = agoCollectGraphParameterBindings(graph); + // Pop one ref from each configured queue and substitute into the graph. + std::vector consumed_refs; + agoApplyQueuedRefsToBindings(graph, pipe, bindings, consumed_refs); + + // Execute the graph synchronously using the normal path. + int status = agoExecuteGraph(graph); + + // Restore original bindings so the next execution sees the static defaults. + agoRestoreBindings(graph, bindings); + + // Release references retained for this execution. + for (AgoData * ref : consumed_refs) { + if (ref) { + agoReleaseData(ref, false); + } + } + + // Move consumed refs to done queues and wake waiters. + agoMoveConsumedRefsToDone(graph); + { + AgoGraphParameterQueue * q0 = agoGetGraphParameterQueue(pipe, 0); + AgoGraphParameterQueue * q1 = agoGetGraphParameterQueue(pipe, 1); + + } + + // Emit node completion events for all user-registered nodes. This covers + // the case where graph optimization rewrote the user-visible nodes. + if (status == VX_SUCCESS) { + agoEmitRegisteredNodeEvents(graph, VX_EVENT_NODE_COMPLETED, VX_SUCCESS); + } + + // Emit graph completion event. + if (status == VX_SUCCESS) { + agoNotifyGraphCompleted(graph); + } + + return status; +} + +// +// QUEUE_MANUAL: drain all ready queues, executing one graph instance per +// complete set of ready refs. +// +static int agoExecuteGraphQueueManual(AgoGraph * graph) +{ + AgoGraphPipeliningState * pipe = graph->pipelining; + if (!pipe) + return VX_FAILURE; + + int overall_status = VX_SUCCESS; + for (;;) { + // Check if every enabled queue has at least one ready ref. + bool all_ready = true; + for (auto& q : pipe->param_queues) { + if (!q->enabled) + continue; + if (q->ready_refs.empty()) { + all_ready = false; + break; + } + } + + if (!all_ready) + break; + + int status = agoExecutePipelinedGraphOnce(graph); + if (status != VX_SUCCESS) { + overall_status = status; + break; + } + } + return overall_status; +} + +// +// Background executor loop for QUEUE_AUTO. +// +static void agoGraphQueueAutoExecutorLoop(AgoGraph * graph) +{ + AgoGraphPipeliningState * pipe = graph->pipelining; + if (!pipe) + return; + + while (!pipe->executor_stop.load()) { + { + CAgoLock lock(graph->cs); + if (pipe->schedule_mode != VX_GRAPH_SCHEDULE_MODE_QUEUE_AUTO) + break; + + // Wait until all enabled queues have at least one ready ref. + bool all_ready = true; + for (auto& q : pipe->param_queues) { + if (!q->enabled) + continue; + if (q->ready_refs.empty()) { + all_ready = false; + break; + } + } + if (all_ready) { + agoExecutePipelinedGraphOnce(graph); + } + } + std::this_thread::sleep_for(std::chrono::milliseconds(1)); + } +} + +// +// Streaming executor loop. +// +static void agoGraphStreamingExecutorLoop(AgoGraph * graph) +{ + AgoGraphPipeliningState * pipe = graph->pipelining; + if (!pipe) + return; + + while (!pipe->streaming_stop.load()) { + { + CAgoLock lock(graph->cs); + if (!pipe->streaming_enabled) + break; + agoExecutePipelinedGraphOnce(graph); + } + std::this_thread::sleep_for(std::chrono::microseconds(100)); + } +} + +// +// Public internal entry point used by agoProcessGraph/agoScheduleGraph when +// the graph is in a pipelining schedule mode. +// +int agoExecuteGraphPipelined(AgoGraph * graph) +{ + if (!agoIsValidGraph(graph)) + return VX_ERROR_INVALID_REFERENCE; + + AgoGraphPipeliningState * pipe = agoGetGraphPipeliningState(graph); + if (!pipe) + return VX_FAILURE; + + if (pipe->schedule_mode == VX_GRAPH_SCHEDULE_MODE_QUEUE_MANUAL) { + return agoExecuteGraphQueueManual(graph); + } + + if (pipe->schedule_mode == VX_GRAPH_SCHEDULE_MODE_QUEUE_AUTO) { + // QUEUE_AUTO runs via the background executor; synchronous entry just + // makes sure any currently queued refs are processed and returns. + // Wait a short while for executor progress. + for (int i = 0; i < 10; i++) { + bool any_ready = false; + for (auto& q : pipe->param_queues) { + if (!q->ready_refs.empty()) { + any_ready = true; + break; + } + } + if (!any_ready) + break; + std::this_thread::sleep_for(std::chrono::milliseconds(1)); + } + return VX_SUCCESS; + } + + // Streaming mode handled by streaming thread, not via this entry. + return VX_SUCCESS; +} + +// +// Start the QUEUE_AUTO background executor if not already running. +// +void agoStartGraphPipeliningAutoExecutor(AgoGraph * graph) +{ + AgoGraphPipeliningState * pipe = agoGetGraphPipeliningState(graph); + if (!pipe) + return; + if (!pipe->executor_thread.joinable()) { + pipe->executor_stop.store(false); + pipe->executor_thread = std::thread([graph]() { + agoGraphQueueAutoExecutorLoop(graph); + }); + } +} + +// +// Start the streaming thread. +// +void agoStartGraphStreamingThread(AgoGraph * graph) +{ + AgoGraphPipeliningState * pipe = agoGetGraphPipeliningState(graph); + if (!pipe) + return; + if (!pipe->streaming_thread.joinable()) { + pipe->streaming_stop.store(false); + pipe->streaming_thread = std::thread([graph]() { + agoGraphStreamingExecutorLoop(graph); + }); + } +} +#else +// Stubs when the pipelining/streaming/event extension is disabled. +AgoGraphPipeliningState * agoGetGraphPipeliningState(AgoGraph *) { return nullptr; } +AgoContextEventSystem * agoGetContextEventSystem(AgoContext *) { return nullptr; } +void agoStopGraphPipelining(AgoGraph *) {} +bool agoGraphHasNodeEventRegistrations(AgoGraph *) { return false; } +void agoPushEvent(AgoContext *, const AgoEvent&) {} +void agoNotifyGraphCompleted(AgoGraph *) {} +void agoNotifyNodeCompleted(AgoGraph *, AgoNode *) {} +void agoNotifyNodeError(AgoGraph *, AgoNode *, vx_status) {} +void agoNotifyGraphParameterConsumed(AgoGraph *, vx_uint32) {} +int agoExecuteGraphPipelined(AgoGraph *) { return VX_ERROR_NOT_SUPPORTED; } +int agoExecutePipelinedGraphOnce(AgoGraph *) { return VX_ERROR_NOT_SUPPORTED; } +void agoStartGraphPipeliningAutoExecutor(AgoGraph *) {} +void agoStartGraphStreamingThread(AgoGraph *) {} +#endif diff --git a/amd_openvx/openvx/ago/ago_platform.h b/amd_openvx/openvx/ago/ago_platform.h index 8adfab027..eeee9c0db 100644 --- a/amd_openvx/openvx/ago/ago_platform.h +++ b/amd_openvx/openvx/ago/ago_platform.h @@ -33,6 +33,7 @@ THE SOFTWARE. #define _USE_MATH_DEFINES #include #include +#include #include #include #include @@ -49,6 +50,10 @@ THE SOFTWARE. #include #include #include +#include +#include +#include +#include #if _WIN32 #include diff --git a/amd_openvx/openvx/ago/ago_util.cpp b/amd_openvx/openvx/ago/ago_util.cpp index b48374412..775579465 100644 --- a/amd_openvx/openvx/ago/ago_util.cpp +++ b/amd_openvx/openvx/ago/ago_util.cpp @@ -3004,6 +3004,7 @@ AgoNode * agoCreateNode(AgoGraph * graph, AgoKernel * kernel) node->attr_affinity = graph->attr_affinity; node->ref.internal_count = 1; node->akernel = kernel; + node->pipeup_output_depth = kernel->pipeup_output_depth; node->attr_border_mode.mode = VX_BORDER_MODE_UNDEFINED; node->localDataSize = kernel->localDataSize; node->localDataPtr = NULL; @@ -3331,7 +3332,8 @@ AgoKernel::AgoKernel() kernel_f{ nullptr }, validate_f{ nullptr }, input_validate_f{ nullptr }, output_validate_f{ nullptr }, initialize_f{ nullptr }, deinitialize_f{ nullptr }, query_target_support_f{ nullptr }, opencl_codegen_callback_f{ nullptr }, regen_callback_f{ nullptr }, opencl_global_work_update_callback_f{ nullptr }, gpu_buffer_update_callback_f{ nullptr }, gpu_buffer_update_param_index{ 0 }, - opencl_buffer_access_enable{ vx_false_e }, importing_module_index_plus1{ 0 } + opencl_buffer_access_enable{ vx_false_e }, importing_module_index_plus1{ 0 }, + pipeup_output_depth{ 1 }, pipeup_input_depth{ 1 } { memset(&name, 0, sizeof(name)); memset(&argConfig, 0, sizeof(argConfig)); @@ -3362,7 +3364,8 @@ AgoSuperNode::~AgoSuperNode() AgoNode::AgoNode() : next{ nullptr }, akernel{ nullptr }, flags{ 0 }, localDataSize{ 0 }, localDataPtr{ nullptr }, localDataPtr_allocated{ nullptr }, valid_rect_reset{ vx_true_e }, valid_rect_num_inputs{ 0 }, valid_rect_num_outputs{ 0 }, valid_rect_inputs{ nullptr }, valid_rect_outputs{ nullptr }, - paramCount{ 0 }, callback{ nullptr }, supernode{ nullptr }, initialized{ false }, target_support_flags{ 0 }, hierarchical_level{ 0 }, status{ VX_SUCCESS } + paramCount{ 0 }, callback{ nullptr }, supernode{ nullptr }, initialized{ false }, target_support_flags{ 0 }, hierarchical_level{ 0 }, status{ VX_SUCCESS }, + node_state{ VX_NODE_STATE_PIPEUP }, node_exec_count{ 0 }, pipeup_output_depth{ 0 } , drama_divide_invoked{ false } #if ENABLE_OPENCL , opencl_type{ 0 }, opencl_param_mem2reg_mask{ 0 }, opencl_param_discard_mask{ 0 }, opencl_param_as_value_mask{ 0 }, @@ -3414,7 +3417,7 @@ AgoGraph::AgoGraph() : next{ nullptr }, hThread{ nullptr }, hSemToThread{ nullptr }, hSemFromThread{ nullptr }, threadScheduleCount{ 0 }, threadExecuteCount{ 0 }, threadWaitCount{ 0 }, threadThreadTerminationState{ 0 }, isReadyToExecute{ vx_false_e }, detectedInvalidNode{ false }, status{ VX_SUCCESS }, - virtualDataGenerationCount{ 0 }, optimizer_flags{ AGO_GRAPH_OPTIMIZER_FLAGS_DEFAULT }, verified{ false }, enable_performance_profiling{ false }, execFrameCount{ 0 } + virtualDataGenerationCount{ 0 }, optimizer_flags{ AGO_GRAPH_OPTIMIZER_FLAGS_DEFAULT }, verified{ false }, enable_performance_profiling{ false }, execFrameCount{ 0 }, pipelining{ nullptr } #if ENABLE_OPENCL , supernodeList{ nullptr }, opencl_cmdq{ nullptr }, opencl_device{ nullptr } , enable_node_level_gpu_flush{ true } @@ -3433,6 +3436,13 @@ AgoGraph::AgoGraph() } AgoGraph::~AgoGraph() { + // stop and cleanup pipelining state + if (pipelining) { + agoStopGraphPipelining(this); + delete pipelining; + pipelining = nullptr; + } + // decrement auto age delays for (auto it = autoAgeDelayList.begin(); it != autoAgeDelayList.end(); it++) { if ((agoIsValidData(*it, VX_TYPE_DELAY) || agoIsValidData(*it, VX_TYPE_OBJECT_ARRAY)) && (*it)->ref.internal_count > 0) @@ -3464,7 +3474,7 @@ AgoGraph::~AgoGraph() AgoContext::AgoContext() : perfNormFactor{ 0 }, dataGenerationCount{ 0 }, nextUserStructId{ VX_TYPE_USER_STRUCT_START }, nextUserKernelId{ 0 }, nextUserLibraryId{ 1 }, num_active_modules{ 0 }, num_active_references{ 0 }, callback_log{ nullptr }, callback_reentrant{ vx_false_e }, - thread_config{ CONFIG_THREAD_DEFAULT }, importing_module_index_plus1{ 0 }, graph_garbage_data{ nullptr }, graph_garbage_node{ nullptr }, graph_garbage_list{ nullptr } + thread_config{ CONFIG_THREAD_DEFAULT }, importing_module_index_plus1{ 0 }, graph_garbage_data{ nullptr }, graph_garbage_node{ nullptr }, graph_garbage_list{ nullptr }, events{ nullptr } #if ENABLE_OPENCL #if defined(CL_VERSION_2_0) , opencl_svmcaps{ 0 } @@ -3568,6 +3578,12 @@ AgoContext::~AgoContext() // remove kernel objects agoResetKernelList(&kernelList); + // cleanup event system + if (events) { + delete events; + events = nullptr; + } + #if ENABLE_OPENCL if (opencl_mem_alloc_count > 0) { agoAddLogEntry(&ref, VX_SUCCESS, "OK: OpenCL buffer usage: " VX_FMT_SIZE ", " VX_FMT_SIZE "/" VX_FMT_SIZE "\n", @@ -3583,3 +3599,24 @@ AgoContext::~AgoContext() // critical section DeleteCriticalSection(&cs); } + +AgoGraphPipeliningState::AgoGraphPipeliningState() + : schedule_mode{ VX_GRAPH_SCHEDULE_MODE_NORMAL }, timeout_ms{ VX_TIMEOUT_WAIT_FOREVER }, + event_timeout_ms{ VX_TIMEOUT_WAIT_FOREVER }, pipeline_depth{ 1 }, + streaming_enabled{ false }, trigger_node{ nullptr }, streaming_stop{ false }, + active_executions{ 0 }, executor_stop{ false } +{ +} + +AgoGraphPipeliningState::~AgoGraphPipeliningState() +{ +} + +AgoContextEventSystem::AgoContextEventSystem() + : enabled{ true }, timeout_ms{ VX_TIMEOUT_WAIT_FOREVER } +{ +} + +AgoContextEventSystem::~AgoContextEventSystem() +{ +} diff --git a/amd_openvx/openvx/api/vx_api.cpp b/amd_openvx/openvx/api/vx_api.cpp index 7b256a4c1..6771642b2 100644 --- a/amd_openvx/openvx/api/vx_api.cpp +++ b/amd_openvx/openvx/api/vx_api.cpp @@ -325,6 +325,13 @@ VX_API_ENTRY vx_status VX_API_CALL vxQueryContext(vx_context context, vx_enum at status = VX_SUCCESS; } break; + case VX_CONTEXT_EVENT_TIMEOUT: + if (size == sizeof(vx_uint32)) { + AgoContextEventSystem * evsys = agoGetContextEventSystem(context); + *(vx_uint32 *)ptr = evsys ? evsys->timeout_ms : VX_TIMEOUT_WAIT_FOREVER; + status = VX_SUCCESS; + } + break; default: status = VX_ERROR_NOT_SUPPORTED; break; @@ -354,6 +361,13 @@ VX_API_ENTRY vx_status VX_API_CALL vxSetContextAttribute(vx_context context, vx_ CAgoLock lock(context->cs); switch (attribute) { + case VX_CONTEXT_EVENT_TIMEOUT: + if (size == sizeof(vx_uint32)) { + AgoContextEventSystem * evsys = agoGetContextEventSystem(context); + evsys->timeout_ms = *(const vx_uint32 *)ptr; + status = VX_SUCCESS; + } + break; case VX_CONTEXT_ATTRIBUTE_IMMEDIATE_BORDER_MODE: if(!ptr) return VX_ERROR_INVALID_PARAMETERS; if (size == sizeof(vx_border_mode_t)) { @@ -2539,6 +2553,18 @@ VX_API_ENTRY vx_status VX_API_CALL vxQueryKernel(vx_kernel kernel, vx_enum attri status = VX_SUCCESS; } break; + case VX_KERNEL_PIPEUP_OUTPUT_DEPTH: + if (size == sizeof(vx_uint32)) { + *(vx_uint32 *)ptr = kernel->pipeup_output_depth; + status = VX_SUCCESS; + } + break; + case VX_KERNEL_PIPEUP_INPUT_DEPTH: + if (size == sizeof(vx_uint32)) { + *(vx_uint32 *)ptr = kernel->pipeup_input_depth; + status = VX_SUCCESS; + } + break; default: status = VX_ERROR_NOT_SUPPORTED; break; @@ -2658,7 +2684,7 @@ VX_API_ENTRY vx_kernel VX_API_CALL vxAddUserKernel(vx_context context, vx_kernel_deinitialize_f deinit) { vx_kernel kernel = NULL; - if (agoIsValidContext(context) && numParams > 0 && numParams <= AGO_MAX_PARAMS && func_ptr && validate) { + if (agoIsValidContext(context) && numParams > 0 && numParams <= AGO_MAX_PARAMS && func_ptr) { CAgoLock lock(context->cs); // make sure there are no kernels with the same name if (!agoFindKernelByEnum(context, enumeration) && !agoFindKernelByName(context, name)) { @@ -2782,9 +2808,9 @@ VX_API_ENTRY vx_status VX_API_CALL vxRemoveKernel(vx_kernel kernel) vx_status status = VX_ERROR_INVALID_REFERENCE; if (agoIsValidKernel(kernel)) { status = VX_ERROR_INVALID_PARAMETERS; - // release if the kernel is not finalized and not a built-in kernel or user kernel with validate_f without external references + // release if the kernel is not finalized or is an externally registered user kernel if (!kernel->finalized || - (kernel->validate_f && kernel->external_kernel && (kernel->flags & AGO_KERNEL_FLAG_GROUP_USER) /*&& + (kernel->external_kernel && (kernel->flags & AGO_KERNEL_FLAG_GROUP_USER) /*&& kernel->ref.internal_count < 2 && kernel->ref.external_count == 0*/)) { CAgoLock lock(kernel->ref.context->cs); @@ -2822,6 +2848,20 @@ VX_API_ENTRY vx_status VX_API_CALL vxSetKernelAttribute(vx_kernel kernel, vx_enu status = VX_SUCCESS; } break; + case VX_KERNEL_PIPEUP_OUTPUT_DEPTH: + if (size == sizeof(vx_uint32)) { + vx_uint32 v = *(const vx_uint32 *)ptr; + if (v < 1 || kernel->finalized) status = VX_ERROR_INVALID_PARAMETERS; + else { kernel->pipeup_output_depth = v; status = VX_SUCCESS; } + } + break; + case VX_KERNEL_PIPEUP_INPUT_DEPTH: + if (size == sizeof(vx_uint32)) { + vx_uint32 v = *(const vx_uint32 *)ptr; + if (v < 1 || kernel->finalized) status = VX_ERROR_INVALID_PARAMETERS; + else { kernel->pipeup_input_depth = v; status = VX_SUCCESS; } + } + break; case VX_KERNEL_ATTRIBUTE_AMD_NODE_REGEN_CALLBACK: if (size == sizeof(void *)) { if (!kernel->finalized) { @@ -3158,6 +3198,39 @@ VX_API_ENTRY vx_status VX_API_CALL vxQueryGraph(vx_graph graph, vx_enum attribut status = VX_SUCCESS; } break; +#if OPENVX_USE_PIPELINING + case VX_GRAPH_SCHEDULE_MODE: + if (size == sizeof(vx_enum)) { + *(vx_enum *)ptr = graph->pipelining ? graph->pipelining->schedule_mode : VX_GRAPH_SCHEDULE_MODE_NORMAL; + status = VX_SUCCESS; + } + break; + case VX_GRAPH_TIMEOUT: + if (size == sizeof(vx_uint32)) { + *(vx_uint32 *)ptr = graph->pipelining ? graph->pipelining->timeout_ms : VX_TIMEOUT_WAIT_FOREVER; + status = VX_SUCCESS; + } + break; + case VX_GRAPH_EVENT_TIMEOUT: + if (size == sizeof(vx_uint32)) { + *(vx_uint32 *)ptr = graph->pipelining ? graph->pipelining->event_timeout_ms : VX_TIMEOUT_WAIT_FOREVER; + status = VX_SUCCESS; + } + break; + case VX_GRAPH_PIPELINE_DEPTH: + if (size == sizeof(vx_uint32)) { + *(vx_uint32 *)ptr = graph->pipelining ? graph->pipelining->pipeline_depth : 1; + status = VX_SUCCESS; + } + break; +#else + case VX_GRAPH_SCHEDULE_MODE: + case VX_GRAPH_TIMEOUT: + case VX_GRAPH_EVENT_TIMEOUT: + case VX_GRAPH_PIPELINE_DEPTH: + status = VX_ERROR_NOT_SUPPORTED; + break; +#endif case VX_GRAPH_ATTRIBUTE_AMD_OPTIMIZER_FLAGS: if (size == sizeof(vx_uint32)) { *(vx_uint32 *)ptr = graph->optimizer_flags; @@ -3247,6 +3320,35 @@ VX_API_ENTRY vx_status VX_API_CALL vxSetGraphAttribute(vx_graph graph, vx_enum a CAgoLock lock(graph->cs); switch (attribute) { +#if OPENVX_USE_PIPELINING + case VX_GRAPH_TIMEOUT: + if (size == sizeof(vx_uint32)) { + AgoGraphPipeliningState * pipe = agoGetGraphPipeliningState(graph); + pipe->timeout_ms = *(const vx_uint32 *)ptr; + status = VX_SUCCESS; + } + break; + case VX_GRAPH_EVENT_TIMEOUT: + if (size == sizeof(vx_uint32)) { + AgoGraphPipeliningState * pipe = agoGetGraphPipeliningState(graph); + pipe->event_timeout_ms = *(const vx_uint32 *)ptr; + status = VX_SUCCESS; + } + break; + case VX_GRAPH_PIPELINE_DEPTH: + if (size == sizeof(vx_uint32)) { + AgoGraphPipeliningState * pipe = agoGetGraphPipeliningState(graph); + pipe->pipeline_depth = *(const vx_uint32 *)ptr; + status = VX_SUCCESS; + } + break; +#else + case VX_GRAPH_TIMEOUT: + case VX_GRAPH_EVENT_TIMEOUT: + case VX_GRAPH_PIPELINE_DEPTH: + status = VX_ERROR_NOT_SUPPORTED; + break; +#endif case VX_GRAPH_ATTRIBUTE_AMD_IMPORT_FROM_TEXT: if (size == sizeof(AgoGraphImportInfo)) { status = VX_SUCCESS; @@ -3494,6 +3596,18 @@ VX_API_ENTRY vx_status VX_API_CALL vxQueryNode(vx_node node, vx_enum attribute, status = VX_SUCCESS; } break; +#if OPENVX_USE_PIPELINING + case VX_NODE_STATE: + if (size == sizeof(vx_uint32)) { + *(vx_uint32 *)ptr = node->node_state; + status = VX_SUCCESS; + } + break; +#else + case VX_NODE_STATE: + status = VX_ERROR_NOT_SUPPORTED; + break; +#endif case VX_NODE_ATTRIBUTE_AMD_AFFINITY: if (size == sizeof(AgoTargetAffinityInfo_)) { *(AgoTargetAffinityInfo_ *)ptr = node->attr_affinity; @@ -3524,7 +3638,6 @@ VX_API_ENTRY vx_status VX_API_CALL vxQueryNode(vx_node node, vx_enum attribute, } break; #endif - default: status = VX_ERROR_NOT_SUPPORTED; break; } @@ -4486,6 +4599,12 @@ VX_API_ENTRY vx_status VX_API_CALL vxQueryReference(vx_reference ref, vx_enum at status = VX_SUCCESS; } break; + case VX_REFERENCE_ENQUEUE_COUNT: + if (size == sizeof(vx_uint32)) { + *(vx_uint32 *)ptr = 0; + status = VX_SUCCESS; + } + break; case VX_REFERENCE_TYPE: if (size == sizeof(vx_enum)) { *(vx_enum *)ptr = ref->type; diff --git a/amd_openvx/openvx/api/vx_pipelining_api.cpp b/amd_openvx/openvx/api/vx_pipelining_api.cpp new file mode 100644 index 000000000..922b1d960 --- /dev/null +++ b/amd_openvx/openvx/api/vx_pipelining_api.cpp @@ -0,0 +1,595 @@ +/* +Copyright (c) 2015 - 2026 Advanced Micro Devices, Inc. All rights reserved. + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. +*/ + +#include "ago_internal.h" + +// +// OpenVX Pipelining Extension - public API implementation +// + +#if OPENVX_USE_PIPELINING + +VX_API_ENTRY vx_status VX_API_CALL vxSetGraphScheduleConfig( + vx_graph graph, + vx_enum graph_schedule_mode, + vx_uint32 graph_parameters_list_size, + const vx_graph_parameter_queue_params_t graph_parameters_queue_params_list[]) +{ + vx_status status = VX_ERROR_INVALID_REFERENCE; + if (agoIsValidGraph(graph) && !graph->verified) { + if ((graph_schedule_mode != VX_GRAPH_SCHEDULE_MODE_NORMAL) && + (graph_schedule_mode != VX_GRAPH_SCHEDULE_MODE_QUEUE_AUTO) && + (graph_schedule_mode != VX_GRAPH_SCHEDULE_MODE_QUEUE_MANUAL)) { + return VX_ERROR_INVALID_PARAMETERS; + } + if (graph_schedule_mode == VX_GRAPH_SCHEDULE_MODE_NORMAL) { + if (graph_parameters_list_size != 0 || graph_parameters_queue_params_list != nullptr) { + return VX_ERROR_INVALID_PARAMETERS; + } + } else { + if (graph_parameters_list_size == 0 || graph_parameters_queue_params_list == nullptr) { + return VX_ERROR_INVALID_PARAMETERS; + } + } + + AgoGraphPipeliningState * pipe = agoGetGraphPipeliningState(graph); + if (!pipe) + return VX_FAILURE; + + CAgoLock lock(graph->cs); + + // Stop any active executor before reconfiguring. + agoStopGraphPipelining(graph); + + pipe->schedule_mode = graph_schedule_mode; + pipe->param_queues.clear(); + pipe->param_queues.resize(graph->parameters.size()); + for (size_t i = 0; i < pipe->param_queues.size(); i++) { if (!pipe->param_queues[i]) pipe->param_queues[i].reset(new AgoGraphParameterQueue()); pipe->param_queues[i]->index = (vx_uint32)i; } + for (vx_uint32 i = 0; i < graph_parameters_list_size; i++) { + const vx_graph_parameter_queue_params_t & p = graph_parameters_queue_params_list[i]; + vx_uint32 index = p.graph_parameter_index; + if (index >= (vx_uint32)graph->parameters.size()) + return VX_ERROR_INVALID_PARAMETERS; + if (p.refs_list_size == 0) + return VX_ERROR_INVALID_PARAMETERS; + pipe->param_queues[index].get()->max_depth = p.refs_list_size; + pipe->param_queues[index].get()->enabled = true; + if (p.refs_list) { + for (vx_uint32 j = 0; j < p.refs_list_size; j++) { + vx_reference ref = p.refs_list[j]; + if (!ref) + return VX_ERROR_INVALID_PARAMETERS; + if (!agoIsValidReference((AgoReference *)ref)) + return VX_ERROR_INVALID_REFERENCE; + pipe->param_queues[index].get()->valid_refs.push_back((AgoData *)ref); + } + } + } + + if (graph_schedule_mode == VX_GRAPH_SCHEDULE_MODE_QUEUE_AUTO) { + agoStartGraphPipeliningAutoExecutor(graph); + } + + status = VX_SUCCESS; + } + return status; +} + +VX_API_ENTRY vx_status VX_API_CALL vxGetGraphParameterRefsList( + vx_graph graph, + vx_uint32 param, + vx_uint32 ref_list_size, + vx_reference refs_list[]) +{ + vx_status status = VX_ERROR_INVALID_REFERENCE; + if (agoIsValidGraph(graph) && graph->verified) { + status = VX_ERROR_INVALID_PARAMETERS; + AgoGraphPipeliningState * pipe = agoGetGraphPipeliningState(graph); + if (pipe && param < (vx_uint32)pipe->param_queues.size() && refs_list) { + AgoGraphParameterQueue * q = pipe->param_queues[param].get(); + if (ref_list_size >= (vx_uint32)q->valid_refs.size()) { + for (size_t i = 0; i < q->valid_refs.size(); i++) { + refs_list[i] = (vx_reference)q->valid_refs[i]; + } + status = VX_SUCCESS; + } + } + } + return status; +} + +VX_API_ENTRY vx_status VX_API_CALL vxAddReferencesToGraphParameterList( + vx_graph graph, + vx_uint32 graph_parameter_index, + vx_uint32 number_to_add, + const vx_reference new_references[]) +{ + vx_status status = VX_ERROR_INVALID_REFERENCE; + if (agoIsValidGraph(graph) && graph->verified) { + status = VX_ERROR_INVALID_PARAMETERS; + if (number_to_add == 0 || !new_references) + return status; + AgoGraphPipeliningState * pipe = agoGetGraphPipeliningState(graph); + if (pipe && graph_parameter_index < (vx_uint32)pipe->param_queues.size()) { + AgoGraphParameterQueue * q = pipe->param_queues[graph_parameter_index].get(); + for (vx_uint32 i = 0; i < number_to_add; i++) { + if (!new_references[i] || !agoIsValidReference((AgoReference *)new_references[i])) + return VX_ERROR_INVALID_REFERENCE; + q->valid_refs.push_back((AgoData *)new_references[i]); + } + status = VX_SUCCESS; + } + } + return status; +} + +VX_API_ENTRY vx_status VX_API_CALL vxGraphParameterEnqueueReadyRef( + vx_graph graph, + vx_uint32 graph_parameter_index, + const vx_reference *refs, + vx_uint32 num_refs) +{ + vx_status status = VX_ERROR_INVALID_REFERENCE; + if (agoIsValidGraph(graph)) { + status = VX_ERROR_INVALID_PARAMETERS; + if (num_refs > 0 && !refs) + return status; + AgoGraphPipeliningState * pipe = agoGetGraphPipeliningState(graph); + if (!pipe) + return VX_FAILURE; + if (graph_parameter_index >= (vx_uint32)pipe->param_queues.size()) + return status; + + AgoGraphParameterQueue * q = pipe->param_queues[graph_parameter_index].get(); + // If the queue is not explicitly enabled by the schedule config, allow + // enqueueing as long as the corresponding graph parameter is a valid output. + if (!q->enabled) { + if (graph_parameter_index >= graph->parameters.size()) + return status; + vx_parameter param = graph->parameters[graph_parameter_index]; + if (!param || param->direction != VX_OUTPUT) + return status; + q->enabled = true; + q->max_depth = num_refs; + } + + for (vx_uint32 i = 0; i < num_refs; i++) { + if (!refs[i] || !agoIsValidReference((AgoReference *)refs[i])) + return VX_ERROR_INVALID_REFERENCE; + // If valid_refs is configured, reject refs not in the list. + if (!q->valid_refs.empty()) { + bool found = false; + for (AgoData * valid : q->valid_refs) { + if ((vx_reference)valid == refs[i]) { + found = true; + break; + } + } + if (!found) + return VX_ERROR_INVALID_PARAMETERS; + } + std::lock_guard lock(q->mtx); + q->ready_refs.push_back((AgoData *)refs[i]); + } + status = VX_SUCCESS; + } + return status; +} + +VX_API_ENTRY vx_status VX_API_CALL vxGraphParameterDequeueDoneRef( + vx_graph graph, + vx_uint32 graph_parameter_index, + vx_reference *refs, + vx_uint32 max_refs, + vx_uint32 *num_refs) +{ + vx_status status = VX_ERROR_INVALID_REFERENCE; + if (agoIsValidGraph(graph)) { + if (!refs || !num_refs) + return VX_ERROR_INVALID_PARAMETERS; + AgoGraphPipeliningState * pipe = agoGetGraphPipeliningState(graph); + if (!pipe) + return VX_FAILURE; + if (graph_parameter_index >= (vx_uint32)pipe->param_queues.size()) + return VX_ERROR_INVALID_PARAMETERS; + + AgoGraphParameterQueue * q = pipe->param_queues[graph_parameter_index].get(); + if (!q->enabled) + return VX_ERROR_INVALID_PARAMETERS; + + std::unique_lock lock(q->mtx); + while (q->done_refs.empty()) { + q->done_cv.wait(lock); + } + vx_uint32 count = 0; + while (count < max_refs && !q->done_refs.empty()) { + refs[count] = (vx_reference)q->done_refs.front(); + q->done_refs.pop_front(); + count++; + } + *num_refs = count; + status = VX_SUCCESS; + } + return status; +} + +VX_API_ENTRY vx_status VX_API_CALL vxGraphParameterCheckDoneRef( + vx_graph graph, + vx_uint32 graph_parameter_index, + vx_uint32 *num_refs) +{ + vx_status status = VX_ERROR_INVALID_REFERENCE; + if (agoIsValidGraph(graph)) { + if (!num_refs) + return VX_ERROR_INVALID_PARAMETERS; + AgoGraphPipeliningState * pipe = agoGetGraphPipeliningState(graph); + if (!pipe) + return VX_FAILURE; + if (graph_parameter_index >= (vx_uint32)pipe->param_queues.size()) + return VX_ERROR_INVALID_PARAMETERS; + + AgoGraphParameterQueue * q = pipe->param_queues[graph_parameter_index].get(); + std::lock_guard lock(q->mtx); + *num_refs = (vx_uint32)q->done_refs.size(); + status = VX_SUCCESS; + } + return status; +} + +// +// Context-level event API +// + +VX_API_ENTRY vx_status VX_API_CALL vxEnableEvents(vx_context context) +{ + if (!agoIsValidContext((AgoContext *)context)) + return VX_ERROR_INVALID_REFERENCE; + AgoContextEventSystem * evsys = agoGetContextEventSystem((AgoContext *)context); + if (!evsys) + return VX_FAILURE; + evsys->enabled = true; + return VX_SUCCESS; +} + +VX_API_ENTRY vx_status VX_API_CALL vxDisableEvents(vx_context context) +{ + if (!agoIsValidContext((AgoContext *)context)) + return VX_ERROR_INVALID_REFERENCE; + AgoContextEventSystem * evsys = agoGetContextEventSystem((AgoContext *)context); + if (!evsys) + return VX_FAILURE; + evsys->enabled = false; + return VX_SUCCESS; +} + +VX_API_ENTRY vx_status VX_API_CALL vxWaitEvent(vx_context context, vx_event_t *event, vx_bool do_not_block) +{ + if (!agoIsValidContext((AgoContext *)context) || !event) + return VX_ERROR_INVALID_REFERENCE; + AgoContextEventSystem * evsys = agoGetContextEventSystem((AgoContext *)context); + if (!evsys) + return VX_FAILURE; + + std::unique_lock lock(evsys->events_mtx); + if (do_not_block == vx_true_e) { + if (evsys->events.empty()) + return VX_FAILURE; + } else { + while (evsys->events.empty()) { + evsys->events_cv.wait(lock); + } + } + if (evsys->events.empty()) + return VX_FAILURE; + + AgoEvent evt = evsys->events.front(); + evsys->events.pop_front(); + lock.unlock(); + + event->type = evt.event_type; + event->timestamp = evt.timestamp; + event->app_value = evt.app_value; + switch (evt.event_type) { + case VX_EVENT_GRAPH_PARAMETER_CONSUMED: + event->event_info.graph_parameter_consumed.graph = (vx_graph)evt.graph; + event->event_info.graph_parameter_consumed.graph_parameter_index = evt.graph_parameter_index; + break; + case VX_EVENT_GRAPH_COMPLETED: + event->event_info.graph_completed.graph = (vx_graph)evt.graph; + break; + case VX_EVENT_NODE_COMPLETED: + event->event_info.node_completed.graph = (vx_graph)evt.graph; + event->event_info.node_completed.node = (vx_node)evt.node; + break; + case VX_EVENT_NODE_ERROR: + event->event_info.node_error.graph = (vx_graph)evt.graph; + event->event_info.node_error.node = (vx_node)evt.node; + event->event_info.node_error.status = evt.status; + break; + case VX_EVENT_USER: + event->event_info.user_event.user_event_parameter = evt.user_parameter; + break; + default: + break; + } + return VX_SUCCESS; +} + +VX_API_ENTRY vx_status VX_API_CALL vxSendUserEvent(vx_context context, vx_uint32 app_value, const void *parameter) +{ + if (!agoIsValidContext((AgoContext *)context)) + return VX_ERROR_INVALID_REFERENCE; + AgoContextEventSystem * evsys = agoGetContextEventSystem((AgoContext *)context); + if (!evsys || !evsys->enabled) + return VX_FAILURE; + + AgoEvent evt; + evt.event_type = VX_EVENT_USER; + evt.timestamp = 0; // could use steady_clock + evt.app_value = app_value; + evt.graph = nullptr; + evt.node = nullptr; + evt.graph_parameter_index = 0; + evt.status = VX_SUCCESS; + evt.user_parameter = (void *)parameter; + agoPushEvent((AgoContext *)context, evt); + return VX_SUCCESS; +} + +VX_API_ENTRY vx_status VX_API_CALL vxRegisterEvent(vx_reference ref, enum vx_event_type_e type, vx_uint32 param, vx_uint32 app_value) +{ + if (!ref || !agoIsValidReference((AgoReference *)ref)) + return VX_ERROR_INVALID_REFERENCE; + AgoReference * r = (AgoReference *)ref; + AgoContextEventSystem * evsys = agoGetContextEventSystem(r->context); + if (!evsys) + return VX_FAILURE; + if (type != VX_EVENT_GRAPH_PARAMETER_CONSUMED && + type != VX_EVENT_GRAPH_COMPLETED && + type != VX_EVENT_NODE_COMPLETED && + type != VX_EVENT_NODE_ERROR) { + return VX_ERROR_NOT_SUPPORTED; + } + + AgoEventRegistration reg; + reg.ref = ref; + reg.event_type = type; + reg.app_value = app_value; + reg.graph_parameter_index = (type == VX_EVENT_GRAPH_PARAMETER_CONSUMED) ? param : 0; + std::lock_guard lock(evsys->registrations_mtx); + evsys->registrations.push_back(reg); + return VX_SUCCESS; +} + +// +// Graph-level event API (forwarded to context-level event system for now). +// + +VX_API_ENTRY vx_status VX_API_CALL vxRegisterGraphEvent(vx_reference graph_or_node, enum vx_event_type_e type, vx_uint32 param, vx_uint32 app_value) +{ + return vxRegisterEvent(graph_or_node, type, param, app_value); +} + +VX_API_ENTRY vx_status VX_API_CALL vxWaitGraphEvent(vx_graph graph, vx_event_t *event, vx_bool do_not_block) +{ + if (!agoIsValidGraph((AgoGraph *)graph) || !event) + return VX_ERROR_INVALID_REFERENCE; + AgoContext * context = ((AgoGraph *)graph)->ref.context; + return vxWaitEvent(context, event, do_not_block); +} + +VX_API_ENTRY vx_status VX_API_CALL vxEnableGraphEvents(vx_graph graph) +{ + if (!agoIsValidGraph((AgoGraph *)graph)) + return VX_ERROR_INVALID_REFERENCE; + AgoContext * context = ((AgoGraph *)graph)->ref.context; + return vxEnableEvents(context); +} + +VX_API_ENTRY vx_status VX_API_CALL vxDisableGraphEvents(vx_graph graph) +{ + if (!agoIsValidGraph((AgoGraph *)graph)) + return VX_ERROR_INVALID_REFERENCE; + AgoContext * context = ((AgoGraph *)graph)->ref.context; + return vxDisableEvents(context); +} + +VX_API_ENTRY vx_status VX_API_CALL vxSendUserGraphEvent(vx_graph graph, vx_uint32 app_value, const void *parameter) +{ + if (!agoIsValidGraph((AgoGraph *)graph)) + return VX_ERROR_INVALID_REFERENCE; + AgoContext * context = ((AgoGraph *)graph)->ref.context; + return vxSendUserEvent(context, app_value, parameter); +} + +// +// Streaming API +// + +VX_API_ENTRY vx_status VX_API_CALL vxEnableGraphStreaming(vx_graph graph, vx_node trigger_node) +{ + vx_status status = VX_ERROR_INVALID_REFERENCE; + if (agoIsValidGraph(graph)) { + AgoGraphPipeliningState * pipe = agoGetGraphPipeliningState(graph); + if (!pipe) + return VX_FAILURE; + CAgoLock lock(graph->cs); + pipe->streaming_enabled = true; + if (trigger_node && agoIsValidNode((AgoNode *)trigger_node)) { + pipe->trigger_node = (AgoNode *)trigger_node; + } + status = VX_SUCCESS; + } + return status; +} + +VX_API_ENTRY vx_status VX_API_CALL vxStartGraphStreaming(vx_graph graph) +{ + vx_status status = VX_ERROR_INVALID_REFERENCE; + if (agoIsValidGraph(graph)) { + if (!graph->verified) + return VX_ERROR_NOT_SUFFICIENT; + AgoGraphPipeliningState * pipe = agoGetGraphPipeliningState(graph); + if (!pipe) + return VX_FAILURE; + CAgoLock lock(graph->cs); + if (!pipe->streaming_enabled) + return VX_FAILURE; + agoStartGraphStreamingThread(graph); + status = VX_SUCCESS; + } + return status; +} + +VX_API_ENTRY vx_status VX_API_CALL vxStopGraphStreaming(vx_graph graph) +{ + vx_status status = VX_ERROR_INVALID_REFERENCE; + if (agoIsValidGraph(graph)) { + AgoGraphPipeliningState * pipe = agoGetGraphPipeliningState(graph); + if (!pipe) + return VX_FAILURE; + CAgoLock lock(graph->cs); + pipe->streaming_stop.store(true); + if (pipe->streaming_thread.joinable()) { + pipe->streaming_thread.join(); + } + pipe->streaming_enabled = false; + status = VX_SUCCESS; + } + return status; +} + +// +// Additional helper API (stub) +// + +VX_API_ENTRY vx_status VX_API_CALL vxGetKernelParameterConfig(vx_kernel kernel, vx_uint32 num_params, vx_kernel_parameter_config_t parameter_config[]) +{ + return VX_ERROR_NOT_SUPPORTED; +} +#else +// Stubs when the pipelining/streaming/event extension is disabled. +VX_API_ENTRY vx_status VX_API_CALL vxSetGraphScheduleConfig( + vx_graph graph, + vx_enum graph_schedule_mode, + vx_uint32 graph_parameters_list_size, + const vx_graph_parameter_queue_params_t graph_parameters_queue_params_list[]) +{ + return VX_ERROR_NOT_SUPPORTED; +} +VX_API_ENTRY vx_status VX_API_CALL vxGetGraphParameterRefsList( + vx_graph graph, + vx_uint32 param, + vx_uint32 ref_list_size, + vx_reference refs_list[]) +{ + return VX_ERROR_NOT_SUPPORTED; +} +VX_API_ENTRY vx_status VX_API_CALL vxAddReferencesToGraphParameterList( + vx_graph graph, + vx_uint32 graph_parameter_index, + vx_uint32 number_to_add, + const vx_reference new_references[]) +{ + return VX_ERROR_NOT_SUPPORTED; +} +VX_API_ENTRY vx_status VX_API_CALL vxGraphParameterEnqueueReadyRef( + vx_graph graph, + vx_uint32 graph_parameter_index, + const vx_reference *refs, + vx_uint32 num_refs) +{ + return VX_ERROR_NOT_SUPPORTED; +} +VX_API_ENTRY vx_status VX_API_CALL vxGraphParameterDequeueDoneRef( + vx_graph graph, + vx_uint32 graph_parameter_index, + vx_reference *refs, + vx_uint32 max_refs, + vx_uint32 *num_refs) +{ + return VX_ERROR_NOT_SUPPORTED; +} +VX_API_ENTRY vx_status VX_API_CALL vxGraphParameterCheckDoneRef( + vx_graph graph, + vx_uint32 graph_parameter_index, + vx_uint32 *num_refs) +{ + return VX_ERROR_NOT_SUPPORTED; +} +VX_API_ENTRY vx_status VX_API_CALL vxEnableEvents(vx_context context) +{ + return VX_ERROR_NOT_SUPPORTED; +} +VX_API_ENTRY vx_status VX_API_CALL vxDisableEvents(vx_context context) +{ + return VX_ERROR_NOT_SUPPORTED; +} +VX_API_ENTRY vx_status VX_API_CALL vxWaitEvent(vx_context context, vx_event_t *event, vx_bool do_not_block) +{ + return VX_ERROR_NOT_SUPPORTED; +} +VX_API_ENTRY vx_status VX_API_CALL vxSendUserEvent(vx_context context, vx_uint32 app_value, const void *parameter) +{ + return VX_ERROR_NOT_SUPPORTED; +} +VX_API_ENTRY vx_status VX_API_CALL vxRegisterEvent(vx_reference ref, enum vx_event_type_e type, vx_uint32 param, vx_uint32 app_value) +{ + return VX_ERROR_NOT_SUPPORTED; +} +VX_API_ENTRY vx_status VX_API_CALL vxRegisterGraphEvent(vx_reference graph_or_node, enum vx_event_type_e type, vx_uint32 param, vx_uint32 app_value) +{ + return VX_ERROR_NOT_SUPPORTED; +} +VX_API_ENTRY vx_status VX_API_CALL vxWaitGraphEvent(vx_graph graph, vx_event_t *event, vx_bool do_not_block) +{ + return VX_ERROR_NOT_SUPPORTED; +} +VX_API_ENTRY vx_status VX_API_CALL vxEnableGraphEvents(vx_graph graph) +{ + return VX_ERROR_NOT_SUPPORTED; +} +VX_API_ENTRY vx_status VX_API_CALL vxDisableGraphEvents(vx_graph graph) +{ + return VX_ERROR_NOT_SUPPORTED; +} +VX_API_ENTRY vx_status VX_API_CALL vxSendUserGraphEvent(vx_graph graph, vx_uint32 app_value, const void *parameter) +{ + return VX_ERROR_NOT_SUPPORTED; +} +VX_API_ENTRY vx_status VX_API_CALL vxEnableGraphStreaming(vx_graph graph, vx_node trigger_node) +{ + return VX_ERROR_NOT_SUPPORTED; +} +VX_API_ENTRY vx_status VX_API_CALL vxStartGraphStreaming(vx_graph graph) +{ + return VX_ERROR_NOT_SUPPORTED; +} +VX_API_ENTRY vx_status VX_API_CALL vxStopGraphStreaming(vx_graph graph) +{ + return VX_ERROR_NOT_SUPPORTED; +} +VX_API_ENTRY vx_status VX_API_CALL vxGetKernelParameterConfig(vx_kernel kernel, vx_uint32 num_params, vx_kernel_parameter_config_t parameter_config[]) +{ + return VX_ERROR_NOT_SUPPORTED; +} +#endif From 60d379e35deaaf96247a090b7237afcfab16296b Mon Sep 17 00:00:00 2001 From: Kiriti Gowda Date: Mon, 27 Jul 2026 11:37:25 -0700 Subject: [PATCH 02/19] CI: enable OpenVX pipelining and streaming conformance tests on CPU and HIP - Build the Khronos OpenVX-CTS with OPENVX_USE_PIPELINING=ON and OPENVX_USE_STREAMING=ON in both conformance.yml and conformance-hip.yml. - Add pipelining-cpu / streaming-cpu jobs to the CPU workflow. - Add pipelining-hip / streaming-hip jobs to the HIP workflow, exercising both CPU and GPU AGO targets. - Pipelining jobs run GraphPipeline.* with a 90-minute timeout. - Streaming jobs run GraphStreaming.* with a 15-minute timeout. --- .github/workflows/conformance-hip.yml | 84 +++++++++++++++++++++++++++ .github/workflows/conformance.yml | 58 +++++++++++++++++- 2 files changed, 141 insertions(+), 1 deletion(-) diff --git a/.github/workflows/conformance-hip.yml b/.github/workflows/conformance-hip.yml index a955c92cc..1bcb689fb 100644 --- a/.github/workflows/conformance-hip.yml +++ b/.github/workflows/conformance-hip.yml @@ -284,6 +284,8 @@ jobs: -DOPENVX_INCLUDES="$GITHUB_WORKSPACE/amd_openvx/openvx/include" \ -DOPENVX_LIBRARIES="$GITHUB_WORKSPACE/install/lib/libopenvx.so;$GITHUB_WORKSPACE/install/lib/libvxu.so;pthread;dl;m;rt" \ -DOPENVX_CONFORMANCE_VISION=ON \ + -DOPENVX_USE_PIPELINING=ON \ + -DOPENVX_USE_STREAMING=ON \ -DCMAKE_C_STANDARD_LIBRARIES="-L${ROCM_PATH}/lib -lamdhip64" \ -DCMAKE_CXX_STANDARD_LIBRARIES="-L${ROCM_PATH}/lib -lamdhip64" \ -DCMAKE_EXE_LINKER_FLAGS="-Wl,-rpath-link,${ROCM_PATH}/lib" @@ -1031,6 +1033,88 @@ jobs: path: profraw/ if-no-files-found: ignore + pipelining-hip: + name: Pipelining (HIP) + runs-on: + group: linux-shark42-runner-group + container: + image: mivisionx/ubuntu-24.04:rocm-20260710 + options: --device /dev/kfd --device /dev/dri + needs: build-hip-debug + steps: + - name: Download debug build artifacts + uses: actions/download-artifact@v4 + with: + name: build-artifacts-hip-debug + - name: Validate ROCm + run: | + rocminfo + amd-smi || true + - name: Run pipelining tests + run: | + mkdir -p profraw + chmod +x OpenVX-cts/build/bin/vx_test_conformance + cd OpenVX-cts/build + export LD_LIBRARY_PATH=$GITHUB_WORKSPACE/install/lib:${ROCM_PATH}/lib:$GITHUB_WORKSPACE/OpenVX-cts/build/lib + export VX_TEST_DATA_PATH=$GITHUB_WORKSPACE/OpenVX-cts/test_data/ + export HIP_VISIBLE_DEVICES=0 # ROCR_VISIBLE_DEVICES already pins the assigned GPU; the runner also sets HIP_VISIBLE_DEVICES to the same host index, which stacks and yields hipErrorNoDevice on index-1 GPUs. After ROCR filtering the device is index 0 for HIP. + export AGO_LOG_STDERR=1 + export LLVM_PROFILE_FILE=$GITHUB_WORKSPACE/profraw/pipelining_%p.profraw + for target in CPU GPU; do + echo "::group::pipelining (AGO_DEFAULT_TARGET=$target)" + AGO_DEFAULT_TARGET=$target timeout 5400 ./bin/vx_test_conformance --filter="GraphPipeline.*" + echo "::endgroup::" + done + - name: Upload profraw + if: always() + continue-on-error: true + uses: actions/upload-artifact@v4 + with: + name: profraw-hip-pipelining + path: profraw/ + if-no-files-found: ignore + + streaming-hip: + name: Streaming (HIP) + runs-on: + group: linux-shark42-runner-group + container: + image: mivisionx/ubuntu-24.04:rocm-20260710 + options: --device /dev/kfd --device /dev/dri + needs: build-hip-debug + steps: + - name: Download debug build artifacts + uses: actions/download-artifact@v4 + with: + name: build-artifacts-hip-debug + - name: Validate ROCm + run: | + rocminfo + amd-smi || true + - name: Run streaming tests + run: | + mkdir -p profraw + chmod +x OpenVX-cts/build/bin/vx_test_conformance + cd OpenVX-cts/build + export LD_LIBRARY_PATH=$GITHUB_WORKSPACE/install/lib:${ROCM_PATH}/lib:$GITHUB_WORKSPACE/OpenVX-cts/build/lib + export VX_TEST_DATA_PATH=$GITHUB_WORKSPACE/OpenVX-cts/test_data/ + export HIP_VISIBLE_DEVICES=0 # ROCR_VISIBLE_DEVICES already pins the assigned GPU; the runner also sets HIP_VISIBLE_DEVICES to the same host index, which stacks and yields hipErrorNoDevice on index-1 GPUs. After ROCR filtering the device is index 0 for HIP. + export AGO_LOG_STDERR=1 + export LLVM_PROFILE_FILE=$GITHUB_WORKSPACE/profraw/streaming_%p.profraw + for target in CPU GPU; do + echo "::group::streaming (AGO_DEFAULT_TARGET=$target)" + AGO_DEFAULT_TARGET=$target timeout 900 ./bin/vx_test_conformance --filter="GraphStreaming.*" + echo "::endgroup::" + done + - name: Upload profraw + if: always() + continue-on-error: true + uses: actions/upload-artifact@v4 + with: + name: profraw-hip-streaming + path: profraw/ + if-no-files-found: ignore + # --- Local Tests (API + GDF) --- api-tests-hip: diff --git a/.github/workflows/conformance.yml b/.github/workflows/conformance.yml index 0f108ad6a..44f404a82 100644 --- a/.github/workflows/conformance.yml +++ b/.github/workflows/conformance.yml @@ -158,7 +158,9 @@ jobs: -DCMAKE_POLICY_VERSION_MINIMUM=3.5 \ -DOPENVX_INCLUDES="${{ github.workspace }}/amd_openvx/openvx/include" \ -DOPENVX_LIBRARIES="${INSTALL_PREFIX}/lib/libopenvx.so;${INSTALL_PREFIX}/lib/libvxu.so;pthread;dl;m;rt" \ - -DOPENVX_CONFORMANCE_VISION=ON + -DOPENVX_CONFORMANCE_VISION=ON \ + -DOPENVX_USE_PIPELINING=ON \ + -DOPENVX_USE_STREAMING=ON make -j$(nproc) - name: Upload debug build artifacts @@ -637,6 +639,60 @@ jobs: path: profraw/ if-no-files-found: ignore + pipelining-cpu: + name: Pipelining (CPU) + runs-on: ubuntu-24.04 + needs: build-debug + steps: + - uses: actions/checkout@v4 + - name: Download debug build artifacts + uses: actions/download-artifact@v4 + with: + name: build-artifacts-debug + - name: Run pipelining tests + run: | + mkdir -p profraw + chmod +x OpenVX-cts/build/bin/vx_test_conformance + cd OpenVX-cts/build + export LD_LIBRARY_PATH=${{ github.workspace }}/install/lib:${{ github.workspace }}/OpenVX-cts/build/lib + export VX_TEST_DATA_PATH=${{ github.workspace }}/OpenVX-cts/test_data/ + export LLVM_PROFILE_FILE=${{ github.workspace }}/profraw/pipelining_%p.profraw + timeout 5400 ./bin/vx_test_conformance --filter="GraphPipeline.*" + - name: Upload profraw + if: always() + uses: actions/upload-artifact@v4 + with: + name: profraw-pipelining + path: profraw/ + if-no-files-found: ignore + + streaming-cpu: + name: Streaming (CPU) + runs-on: ubuntu-24.04 + needs: build-debug + steps: + - uses: actions/checkout@v4 + - name: Download debug build artifacts + uses: actions/download-artifact@v4 + with: + name: build-artifacts-debug + - name: Run streaming tests + run: | + mkdir -p profraw + chmod +x OpenVX-cts/build/bin/vx_test_conformance + cd OpenVX-cts/build + export LD_LIBRARY_PATH=${{ github.workspace }}/install/lib:${{ github.workspace }}/OpenVX-cts/build/lib + export VX_TEST_DATA_PATH=${{ github.workspace }}/OpenVX-cts/test_data/ + export LLVM_PROFILE_FILE=${{ github.workspace }}/profraw/streaming_%p.profraw + timeout 900 ./bin/vx_test_conformance --filter="GraphStreaming.*" + - name: Upload profraw + if: always() + uses: actions/upload-artifact@v4 + with: + name: profraw-streaming + path: profraw/ + if-no-files-found: ignore + # --- Local Tests (API + GDF) --- api-tests: From 2419f987ad403899a711f4d18504ff36c3e42849 Mon Sep 17 00:00:00 2001 From: Kiriti Gowda Date: Mon, 27 Jul 2026 15:30:27 -0700 Subject: [PATCH 03/19] Fix regression: only infer output metadata for user kernels without validate The previous change inferred output metadata for every kernel without a validate callback, including built-in kernels that rely on upstream nodes or default meta resolution. This corrupted virtual image metadata and broke vxVerifyGraph for any graph with virtual images/arrays/pyramids (e.g., Graph.GraphFactory, Graph.VirtualImage, Graph.VirtualArray, vision-arithmetic, vision-pyramid, and several GDF tests). Restrict the inference to user kernels only, which is the intended use case for the OpenVX pipelining extension's source/sink kernels. --- amd_openvx/openvx/ago/ago_interface.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/amd_openvx/openvx/ago/ago_interface.cpp b/amd_openvx/openvx/ago/ago_interface.cpp index 6544f096a..aac94c6a7 100644 --- a/amd_openvx/openvx/ago/ago_interface.cpp +++ b/amd_openvx/openvx/ago/ago_interface.cpp @@ -1370,7 +1370,7 @@ vx_status agoVerifyNode(AgoNode * node) vx_meta_format meta = &node->metaList[arg]; // For user kernels without a validate callback, infer output meta from the // bound object so source/sink kernels can verify. - if (!kernel->validate_f && data && kernel->argType[arg] && kernel->argType[arg] != VX_TYPE_REFERENCE) { + if (kernel->user_kernel && !kernel->validate_f && data && kernel->argType[arg] && kernel->argType[arg] != VX_TYPE_REFERENCE) { meta->data.ref.type = data->ref.type; meta->data.u = data->u; } From b6af1ee8b7745f5f63398a080aa021323bad2fec Mon Sep 17 00:00:00 2001 From: Kiriti Gowda Date: Mon, 27 Jul 2026 16:17:20 -0700 Subject: [PATCH 04/19] Fix pipelining queue mutex races agoApplyQueuedRefsToBindings and the all-ready checks in the manual and auto executor loops were accessing ready_refs without holding the queue mutex, causing data races and (on some runs) the fatal glibc mutex-owner assertion during GraphPipeline.UniformImage on the CI runner. Lock q->mtx around all ready_refs/consumed_refs accesses. Also remove a leftover empty debug block. Verified: GraphPipeline.* + GraphStreaming.* combined 133/133 pass locally; legacy graph repro and And_alt.gdf still pass. --- amd_openvx/openvx/ago/ago_interface.cpp | 18 ++++++++++----- amd_openvx/openvx/ago/ago_pipelining.cpp | 29 +++++++++++++----------- 2 files changed, 28 insertions(+), 19 deletions(-) diff --git a/amd_openvx/openvx/ago/ago_interface.cpp b/amd_openvx/openvx/ago/ago_interface.cpp index aac94c6a7..2c013e8be 100644 --- a/amd_openvx/openvx/ago/ago_interface.cpp +++ b/amd_openvx/openvx/ago/ago_interface.cpp @@ -3000,9 +3000,12 @@ int agoWaitGraph(AgoGraph * graph) for (;;) { bool any_ready = false; for (auto& q : pipe->param_queues) { - if (q->enabled && !q->ready_refs.empty()) { - any_ready = true; - break; + if (q->enabled) { + std::lock_guard qlock(q->mtx); + if (!q->ready_refs.empty()) { + any_ready = true; + break; + } } } if (!any_ready) @@ -3023,9 +3026,12 @@ int agoWaitGraph(AgoGraph * graph) for (;;) { bool any_ready = false; for (auto& q : pipe->param_queues) { - if (q->enabled && !q->ready_refs.empty()) { - any_ready = true; - break; + if (q->enabled) { + std::lock_guard qlock(q->mtx); + if (!q->ready_refs.empty()) { + any_ready = true; + break; + } } } if (!any_ready) diff --git a/amd_openvx/openvx/ago/ago_pipelining.cpp b/amd_openvx/openvx/ago/ago_pipelining.cpp index 40134fb81..cb6c17bfa 100644 --- a/amd_openvx/openvx/ago/ago_pipelining.cpp +++ b/amd_openvx/openvx/ago/ago_pipelining.cpp @@ -367,17 +367,23 @@ static void agoApplyQueuedRefsToBindings(AgoGraph * graph, consumed_refs.assign(bindings.size(), nullptr); for (size_t i = 0; i < bindings.size(); i++) { AgoGraphParameterQueue * q = agoGetGraphParameterQueue(pipe, (vx_uint32)i); - - if (!q || q->ready_refs.empty()) { + if (!q) continue; + + AgoData * ref = nullptr; + { + std::lock_guard lock(q->mtx); + if (!q->ready_refs.empty()) { + ref = q->ready_refs.front(); + q->ready_refs.pop_front(); + q->consumed_refs.push_back(ref); + } } - AgoData * ref = q->ready_refs.front(); - q->ready_refs.pop_front(); - q->consumed_refs.push_back(ref); + if (!ref) + continue; + consumed_refs[i] = ref; - if (ref) { - agoRetainData(graph, ref, false); - } + agoRetainData(graph, ref, false); bindings[i].queued = ref; if (bindings[i].original) { agoApplyDataRefSwapWithSiblings(graph, bindings[i].original, ref); @@ -446,11 +452,6 @@ int agoExecutePipelinedGraphOnce(AgoGraph * graph) // Move consumed refs to done queues and wake waiters. agoMoveConsumedRefsToDone(graph); - { - AgoGraphParameterQueue * q0 = agoGetGraphParameterQueue(pipe, 0); - AgoGraphParameterQueue * q1 = agoGetGraphParameterQueue(pipe, 1); - - } // Emit node completion events for all user-registered nodes. This covers // the case where graph optimization rewrote the user-visible nodes. @@ -483,6 +484,7 @@ static int agoExecuteGraphQueueManual(AgoGraph * graph) for (auto& q : pipe->param_queues) { if (!q->enabled) continue; + std::lock_guard lock(q->mtx); if (q->ready_refs.empty()) { all_ready = false; break; @@ -521,6 +523,7 @@ static void agoGraphQueueAutoExecutorLoop(AgoGraph * graph) for (auto& q : pipe->param_queues) { if (!q->enabled) continue; + std::lock_guard qlock(q->mtx); if (q->ready_refs.empty()) { all_ready = false; break; From 5167d491b042afd9368901abc48bfda560d51e50 Mon Sep 17 00:00:00 2001 From: Kiriti Gowda Date: Mon, 27 Jul 2026 18:13:16 -0700 Subject: [PATCH 05/19] Fix remaining queue mutex races and event-system thread safety - Lock q->mtx in the QUEUE_AUTO poll path of agoExecuteGraphPipelined, which raced with the background executor and could corrupt the std::deque / adjacent mutex state seen as the pthread_mutex_owner assertion. - Initialize the context event system eagerly in AgoContext construction and leave it disabled by default; vxEnableEvents toggles it on. This removes the racy lazy creation path and avoids emitting/logging events unless the application explicitly enables them. Verified: full GraphPipeline.* + GraphStreaming.* combined = 133/133 pass; legacy graph repro and And_alt.gdf still pass. --- amd_openvx/openvx/ago/ago_pipelining.cpp | 1 + amd_openvx/openvx/ago/ago_util.cpp | 4 ++-- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/amd_openvx/openvx/ago/ago_pipelining.cpp b/amd_openvx/openvx/ago/ago_pipelining.cpp index cb6c17bfa..2ef945dc1 100644 --- a/amd_openvx/openvx/ago/ago_pipelining.cpp +++ b/amd_openvx/openvx/ago/ago_pipelining.cpp @@ -581,6 +581,7 @@ int agoExecuteGraphPipelined(AgoGraph * graph) for (int i = 0; i < 10; i++) { bool any_ready = false; for (auto& q : pipe->param_queues) { + std::lock_guard qlock(q->mtx); if (!q->ready_refs.empty()) { any_ready = true; break; diff --git a/amd_openvx/openvx/ago/ago_util.cpp b/amd_openvx/openvx/ago/ago_util.cpp index 775579465..e056c9a01 100644 --- a/amd_openvx/openvx/ago/ago_util.cpp +++ b/amd_openvx/openvx/ago/ago_util.cpp @@ -3474,7 +3474,7 @@ AgoGraph::~AgoGraph() AgoContext::AgoContext() : perfNormFactor{ 0 }, dataGenerationCount{ 0 }, nextUserStructId{ VX_TYPE_USER_STRUCT_START }, nextUserKernelId{ 0 }, nextUserLibraryId{ 1 }, num_active_modules{ 0 }, num_active_references{ 0 }, callback_log{ nullptr }, callback_reentrant{ vx_false_e }, - thread_config{ CONFIG_THREAD_DEFAULT }, importing_module_index_plus1{ 0 }, graph_garbage_data{ nullptr }, graph_garbage_node{ nullptr }, graph_garbage_list{ nullptr }, events{ nullptr } + thread_config{ CONFIG_THREAD_DEFAULT }, importing_module_index_plus1{ 0 }, graph_garbage_data{ nullptr }, graph_garbage_node{ nullptr }, graph_garbage_list{ nullptr }, events{ new AgoContextEventSystem() } #if ENABLE_OPENCL #if defined(CL_VERSION_2_0) , opencl_svmcaps{ 0 } @@ -3613,7 +3613,7 @@ AgoGraphPipeliningState::~AgoGraphPipeliningState() } AgoContextEventSystem::AgoContextEventSystem() - : enabled{ true }, timeout_ms{ VX_TIMEOUT_WAIT_FOREVER } + : enabled{ false }, timeout_ms{ VX_TIMEOUT_WAIT_FOREVER } { } From 41c7a7f692fa1efabc9f16ff594673620e3220e3 Mon Sep 17 00:00:00 2001 From: Kiriti Gowda Date: Tue, 28 Jul 2026 02:44:13 -0700 Subject: [PATCH 06/19] fix(pipelining): address Copilot review comments and flaky ManualSchedule race Changes: - Guard VX_CONTEXT_EVENT_TIMEOUT/VX_KERNEL_PIPEUP_* when pipelining disabled. - Restore default labels in vxQueryNode/vxSetNodeAttribute switches. - Add AgoGraphParameterQueue constructor and initialize AgoData::children cleanup. - Use atomic graph thread counters; fix semaphore lost-wakeup in WaitForSingleObject. - Remove spurious semaphore releases in agoWaitGraph that caused phantom executions. - Implement VX_REFERENCE_ENQUEUE_COUNT and use graph timeout in dequeue. - Add timeout support to vxWaitEvent via evsys->timeout_ms. - Clean up event registrations in AgoGraph/AgoNode/AgoData destructors. - Replace QUEUE_AUTO polling with condition-variable wakeups. - Remove debug prints and bogus -DOPENVX_USE_STREAMING=ON CI flags. Verification: - GraphPipeline.* + GraphStreaming.* = 133/133 pass (no ASan leaks). - ManualSchedule/8 stress run = 20/20 pass. - Legacy Graph.VirtualImage repro and And_alt.gdf still pass. --- .github/workflows/conformance-hip.yml | 1 - .github/workflows/conformance.yml | 3 +- amd_openvx/openvx/ago/ago_interface.cpp | 32 ++++----- amd_openvx/openvx/ago/ago_internal.h | 8 ++- amd_openvx/openvx/ago/ago_pipelining.cpp | 72 +++++++++++++++------ amd_openvx/openvx/ago/ago_platform.cpp | 7 +- amd_openvx/openvx/ago/ago_util.cpp | 13 +++- amd_openvx/openvx/api/vx_api.cpp | 17 ++++- amd_openvx/openvx/api/vx_pipelining_api.cpp | 22 +++++-- 9 files changed, 126 insertions(+), 49 deletions(-) diff --git a/.github/workflows/conformance-hip.yml b/.github/workflows/conformance-hip.yml index b2f6c4f28..910255a3e 100644 --- a/.github/workflows/conformance-hip.yml +++ b/.github/workflows/conformance-hip.yml @@ -285,7 +285,6 @@ jobs: -DOPENVX_LIBRARIES="$GITHUB_WORKSPACE/install/lib/libopenvx.so;$GITHUB_WORKSPACE/install/lib/libvxu.so;pthread;dl;m;rt" \ -DOPENVX_CONFORMANCE_VISION=ON \ -DOPENVX_USE_PIPELINING=ON \ - -DOPENVX_USE_STREAMING=ON \ -DCMAKE_C_STANDARD_LIBRARIES="-L${ROCM_PATH}/lib -lamdhip64" \ -DCMAKE_CXX_STANDARD_LIBRARIES="-L${ROCM_PATH}/lib -lamdhip64" \ -DCMAKE_EXE_LINKER_FLAGS="-Wl,-rpath-link,${ROCM_PATH}/lib" diff --git a/.github/workflows/conformance.yml b/.github/workflows/conformance.yml index 44f404a82..2aac6d736 100644 --- a/.github/workflows/conformance.yml +++ b/.github/workflows/conformance.yml @@ -159,8 +159,7 @@ jobs: -DOPENVX_INCLUDES="${{ github.workspace }}/amd_openvx/openvx/include" \ -DOPENVX_LIBRARIES="${INSTALL_PREFIX}/lib/libopenvx.so;${INSTALL_PREFIX}/lib/libvxu.so;pthread;dl;m;rt" \ -DOPENVX_CONFORMANCE_VISION=ON \ - -DOPENVX_USE_PIPELINING=ON \ - -DOPENVX_USE_STREAMING=ON + -DOPENVX_USE_PIPELINING=ON make -j$(nproc) - name: Upload debug build artifacts diff --git a/amd_openvx/openvx/ago/ago_interface.cpp b/amd_openvx/openvx/ago/ago_interface.cpp index 2c013e8be..26a642d88 100644 --- a/amd_openvx/openvx/ago/ago_interface.cpp +++ b/amd_openvx/openvx/ago/ago_interface.cpp @@ -31,20 +31,24 @@ static void agoGraphThreadFunction(LPVOID graph_) #endif { AgoGraph * graph = (AgoGraph *)graph_; - while (WaitForSingleObject(graph->hSemToThread, INFINITE) == WAIT_OBJECT_0) { - graph->threadThreadWaitState = 2; - if (graph->threadThreadTerminationState) + for (;;) { + DWORD w = WaitForSingleObject(graph->hSemToThread, INFINITE); + graph->threadThreadWaitState.store(2); + if (graph->threadThreadTerminationState.load()) break; + // Ignore spurious/error returns from the semaphore wait and retry. + if (w != WAIT_OBJECT_0) + continue; // execute graph graph->status = agoProcessGraph(graph); // inform caller - graph->threadExecuteCount++; + graph->threadExecuteCount.fetch_add(1); ReleaseSemaphore(graph->hSemFromThread, 1, nullptr); } // inform caller about termination - graph->threadThreadTerminationState = 2; + graph->threadThreadTerminationState.store(2); ReleaseSemaphore(graph->hSemFromThread, 1, nullptr); #if _WIN32 return 0; @@ -171,9 +175,9 @@ int agoReleaseGraph(AgoGraph * agraph) EnterCriticalSection(&agraph->cs); // stop graph thread if (agraph->hThread) { - agraph->threadThreadTerminationState = 1; + agraph->threadThreadTerminationState.store(1); ReleaseSemaphore(agraph->hSemToThread, 1, nullptr); - while (agraph->threadThreadTerminationState == 1) { + while (agraph->threadThreadTerminationState.load() == 1) { // give a chance for the thread to run in case it is waititng std::this_thread::sleep_for(std::chrono::milliseconds(1)); ReleaseSemaphore(agraph->hSemToThread, 1, nullptr); @@ -2951,7 +2955,7 @@ int agoScheduleGraph(AgoGraph * graph) vx_status status = VX_ERROR_INVALID_REFERENCE; if (agoIsValidGraph(graph)) { status = VX_SUCCESS; - graph->threadScheduleCount++; + graph->threadScheduleCount.fetch_add(1); if (graph->hThread) { if (!graph->verified) { // make sure to verify the graph in master thread @@ -3045,18 +3049,16 @@ int agoWaitGraph(AgoGraph * graph) return status; } } - graph->threadWaitCount++; - if (graph->threadScheduleCount <= 0) // the graph was never scheduled so return VX_FAILURE + graph->threadWaitCount.fetch_add(1); + if (graph->threadScheduleCount.load() <= 0) // the graph was never scheduled so return VX_FAILURE return VX_FAILURE; if (graph->hThread) { - graph->threadThreadWaitState = 1; - while (graph->threadThreadWaitState == 1) { + graph->threadThreadWaitState.store(1); + while (graph->threadThreadWaitState.load() == 1) { // wait for the agoGraphThreadFunction to be done std::this_thread::sleep_for(std::chrono::milliseconds(1)); - // release the semaphore in case the agoScheduleGraph was called before the agoGraphThreadFunction - ReleaseSemaphore(graph->hSemToThread, 1, nullptr); } - while (graph->threadExecuteCount < graph->threadScheduleCount) { + while (graph->threadExecuteCount.load() < graph->threadScheduleCount.load()) { if (WaitForSingleObject(graph->hSemFromThread, INFINITE) != WAIT_OBJECT_0) { agoAddLogEntry(&graph->ref, VX_FAILURE, "ERROR: agoWaitGraph: WaitForSingleObject failed\n"); status = VX_FAILURE; diff --git a/amd_openvx/openvx/ago/ago_internal.h b/amd_openvx/openvx/ago/ago_internal.h index 3447302f0..9d9c301d4 100644 --- a/amd_openvx/openvx/ago/ago_internal.h +++ b/amd_openvx/openvx/ago/ago_internal.h @@ -678,6 +678,7 @@ struct AgoGraphParameterQueue { vx_uint32 index; vx_uint32 max_depth; bool enabled; + AgoGraphParameterQueue() : index(0), max_depth(0), enabled(false) {} }; struct AgoGraphPipeliningState { @@ -695,6 +696,8 @@ struct AgoGraphPipeliningState { std::mutex execution_mtx; std::thread executor_thread; std::atomic executor_stop; + std::mutex enqueue_mtx; + std::condition_variable enqueue_cv; std::vector> param_queues; public: AgoGraphPipeliningState(); @@ -738,7 +741,7 @@ struct AgoGraph { AgoGraph * next; CRITICAL_SECTION cs; HANDLE hThread, hSemToThread, hSemFromThread; - vx_int32 threadScheduleCount, threadExecuteCount, threadWaitCount, threadThreadTerminationState, threadThreadWaitState; + std::atomic threadScheduleCount, threadExecuteCount, threadWaitCount, threadThreadTerminationState, threadThreadWaitState; AgoDataList dataList; AgoNodeList nodeList; vx_bool isReadyToExecute; @@ -987,12 +990,15 @@ void agoStartGraphStreamingThread(AgoGraph * graph); void agoPushEvent(AgoContext * context, const AgoEvent& evt); int agoExecuteGraphPipelined(AgoGraph * graph); int agoExecutePipelinedGraphOnce(AgoGraph * graph); +int agoExecuteGraphQueueManual(AgoGraph * graph); // event notifications void agoNotifyGraphCompleted(AgoGraph * graph); void agoNotifyNodeCompleted(AgoGraph * graph, AgoNode * node); void agoNotifyNodeError(AgoGraph * graph, AgoNode * node, vx_status status); void agoNotifyGraphParameterConsumed(AgoGraph * graph, vx_uint32 graph_parameter_index); bool agoGraphHasNodeEventRegistrations(AgoGraph * graph); +vx_uint32 agoGetReferenceEnqueueCount(AgoContext * context, AgoReference * ref); +void agoRemoveEventRegistrations(AgoContext * context, vx_reference ref); #if (ENABLE_OPENCL || ENABLE_HIP) int agoGpuOclAllocBuffers(AgoGraph * graph); diff --git a/amd_openvx/openvx/ago/ago_pipelining.cpp b/amd_openvx/openvx/ago/ago_pipelining.cpp index 2ef945dc1..e1219172b 100644 --- a/amd_openvx/openvx/ago/ago_pipelining.cpp +++ b/amd_openvx/openvx/ago/ago_pipelining.cpp @@ -60,6 +60,10 @@ static void agoStopGraphPipeliningExecutor(AgoGraph * graph) return; pipe->executor_stop.store(true); + { + std::lock_guard lock(pipe->enqueue_mtx); + pipe->enqueue_cv.notify_all(); + } if (pipe->executor_thread.joinable()) { pipe->executor_thread.join(); } @@ -134,7 +138,7 @@ static vx_uint32 agoFindEventAppValue(AgoContext * context, vx_reference ref, vx return 0; } -static void agoRemoveEventRegistrations(AgoContext * context, vx_reference ref) +void agoRemoveEventRegistrations(AgoContext * context, vx_reference ref) { AgoContextEventSystem * evsys = agoGetContextEventSystem(context); if (!evsys) @@ -145,6 +149,32 @@ static void agoRemoveEventRegistrations(AgoContext * context, vx_reference ref) [ref](const AgoEventRegistration& reg) { return reg.ref == ref; }), regs.end()); } +vx_uint32 agoGetReferenceEnqueueCount(AgoContext * context, AgoReference * ref) +{ + if (!context || !ref) + return 0; + CAgoLock lock(context->cs); + vx_uint32 count = 0; + for (AgoGraph * graph = context->graphList.head; graph; graph = graph->next) { + AgoGraphPipeliningState * pipe = graph->pipelining; + if (!pipe) + continue; + for (auto& qptr : pipe->param_queues) { + AgoGraphParameterQueue * q = qptr.get(); + if (!q || !q->enabled) + continue; + std::lock_guard qlock(q->mtx); + for (AgoData * d : q->ready_refs) + if ((AgoReference *)d == ref) ++count; + for (AgoData * d : q->consumed_refs) + if ((AgoReference *)d == ref) ++count; + for (AgoData * d : q->done_refs) + if ((AgoReference *)d == ref) ++count; + } + } + return count; +} + // Event helpers // @@ -471,7 +501,7 @@ int agoExecutePipelinedGraphOnce(AgoGraph * graph) // QUEUE_MANUAL: drain all ready queues, executing one graph instance per // complete set of ready refs. // -static int agoExecuteGraphQueueManual(AgoGraph * graph) +int agoExecuteGraphQueueManual(AgoGraph * graph) { AgoGraphPipeliningState * pipe = graph->pipelining; if (!pipe) @@ -512,6 +542,17 @@ static void agoGraphQueueAutoExecutorLoop(AgoGraph * graph) if (!pipe) return; + auto anyReady = [&pipe]() -> bool { + for (auto& q : pipe->param_queues) { + if (!q->enabled) + continue; + std::lock_guard qlock(q->mtx); + if (!q->ready_refs.empty()) + return true; + } + return false; + }; + while (!pipe->executor_stop.load()) { { CAgoLock lock(graph->cs); @@ -531,9 +572,14 @@ static void agoGraphQueueAutoExecutorLoop(AgoGraph * graph) } if (all_ready) { agoExecutePipelinedGraphOnce(graph); + continue; } } - std::this_thread::sleep_for(std::chrono::milliseconds(1)); + // Nothing to do: block until a ref is enqueued or we're asked to stop. + std::unique_lock elock(pipe->enqueue_mtx); + pipe->enqueue_cv.wait_for(elock, std::chrono::milliseconds(1), [&pipe, &anyReady]() { + return pipe->executor_stop.load() || anyReady(); + }); } } @@ -575,22 +621,8 @@ int agoExecuteGraphPipelined(AgoGraph * graph) } if (pipe->schedule_mode == VX_GRAPH_SCHEDULE_MODE_QUEUE_AUTO) { - // QUEUE_AUTO runs via the background executor; synchronous entry just - // makes sure any currently queued refs are processed and returns. - // Wait a short while for executor progress. - for (int i = 0; i < 10; i++) { - bool any_ready = false; - for (auto& q : pipe->param_queues) { - std::lock_guard qlock(q->mtx); - if (!q->ready_refs.empty()) { - any_ready = true; - break; - } - } - if (!any_ready) - break; - std::this_thread::sleep_for(std::chrono::milliseconds(1)); - } + // QUEUE_AUTO runs via the background executor; synchronous entry has + // nothing to do because the executor wakes whenever refs are enqueued. return VX_SUCCESS; } @@ -644,4 +676,6 @@ int agoExecuteGraphPipelined(AgoGraph *) { return VX_ERROR_NOT_SUPPORTED; } int agoExecutePipelinedGraphOnce(AgoGraph *) { return VX_ERROR_NOT_SUPPORTED; } void agoStartGraphPipeliningAutoExecutor(AgoGraph *) {} void agoStartGraphStreamingThread(AgoGraph *) {} +vx_uint32 agoGetReferenceEnqueueCount(AgoContext *, AgoReference *) { return 0; } +void agoRemoveEventRegistrations(AgoContext *, vx_reference) {} #endif diff --git a/amd_openvx/openvx/ago/ago_platform.cpp b/amd_openvx/openvx/ago/ago_platform.cpp index 3418d7071..1cfd27e7d 100644 --- a/amd_openvx/openvx/ago/ago_platform.cpp +++ b/amd_openvx/openvx/ago/ago_platform.cpp @@ -354,10 +354,9 @@ DWORD WaitForSingleObject(HANDLE h, DWORD dwMilliseconds) vx_semaphore * sem = (vx_semaphore *)h; { unique_lock lk(sem->mtx); - sem->cv.wait(lk); // TBD: implement with timeout - } - { - lock_guard lk(sem->mtx); + // Wait only if the semaphore count is currently zero; otherwise + // a notification that arrived before this wait would be lost. + sem->cv.wait(lk, [&sem]() { return sem->count > 0; }); sem->count--; } } diff --git a/amd_openvx/openvx/ago/ago_util.cpp b/amd_openvx/openvx/ago/ago_util.cpp index e056c9a01..102794b6f 100644 --- a/amd_openvx/openvx/ago/ago_util.cpp +++ b/amd_openvx/openvx/ago/ago_util.cpp @@ -3301,6 +3301,8 @@ AgoData::AgoData() } AgoData::~AgoData() { + if (ref.context) + agoRemoveEventRegistrations(ref.context, (vx_reference)this); #if ENABLE_OPENCL agoGpuOclReleaseData(this); #elif ENABLE_HIP @@ -3314,6 +3316,10 @@ AgoData::~AgoData() agoReleaseMemory(reserved_allocated); reserved_allocated = nullptr; } + if (children) { + delete[] children; + children = nullptr; + } } AgoMetaFormat::AgoMetaFormat() : set_valid_rectangle_callback{ nullptr } @@ -3389,6 +3395,8 @@ AgoNode::AgoNode() } AgoNode::~AgoNode() { + if (ref.context) + agoRemoveEventRegistrations(ref.context, (vx_reference)this); agoShutdownNode(this); if (valid_rect_inputs) { delete[] valid_rect_inputs; @@ -3415,7 +3423,7 @@ AgoNode::~AgoNode() } AgoGraph::AgoGraph() : next{ nullptr }, hThread{ nullptr }, hSemToThread{ nullptr }, hSemFromThread{ nullptr }, - threadScheduleCount{ 0 }, threadExecuteCount{ 0 }, threadWaitCount{ 0 }, threadThreadTerminationState{ 0 }, + threadScheduleCount{ 0 }, threadExecuteCount{ 0 }, threadWaitCount{ 0 }, threadThreadTerminationState{ 0 }, threadThreadWaitState{ 0 }, isReadyToExecute{ vx_false_e }, detectedInvalidNode{ false }, status{ VX_SUCCESS }, virtualDataGenerationCount{ 0 }, optimizer_flags{ AGO_GRAPH_OPTIMIZER_FLAGS_DEFAULT }, verified{ false }, enable_performance_profiling{ false }, execFrameCount{ 0 }, pipelining{ nullptr } #if ENABLE_OPENCL @@ -3443,6 +3451,9 @@ AgoGraph::~AgoGraph() pipelining = nullptr; } + if (ref.context) + agoRemoveEventRegistrations(ref.context, (vx_reference)this); + // decrement auto age delays for (auto it = autoAgeDelayList.begin(); it != autoAgeDelayList.end(); it++) { if ((agoIsValidData(*it, VX_TYPE_DELAY) || agoIsValidData(*it, VX_TYPE_OBJECT_ARRAY)) && (*it)->ref.internal_count > 0) diff --git a/amd_openvx/openvx/api/vx_api.cpp b/amd_openvx/openvx/api/vx_api.cpp index 6771642b2..5288fa9be 100644 --- a/amd_openvx/openvx/api/vx_api.cpp +++ b/amd_openvx/openvx/api/vx_api.cpp @@ -364,8 +364,12 @@ VX_API_ENTRY vx_status VX_API_CALL vxSetContextAttribute(vx_context context, vx_ case VX_CONTEXT_EVENT_TIMEOUT: if (size == sizeof(vx_uint32)) { AgoContextEventSystem * evsys = agoGetContextEventSystem(context); - evsys->timeout_ms = *(const vx_uint32 *)ptr; - status = VX_SUCCESS; + if (evsys) { + evsys->timeout_ms = *(const vx_uint32 *)ptr; + status = VX_SUCCESS; + } else { + status = VX_ERROR_NOT_SUPPORTED; + } } break; case VX_CONTEXT_ATTRIBUTE_IMMEDIATE_BORDER_MODE: @@ -2553,6 +2557,7 @@ VX_API_ENTRY vx_status VX_API_CALL vxQueryKernel(vx_kernel kernel, vx_enum attri status = VX_SUCCESS; } break; +#if OPENVX_USE_PIPELINING case VX_KERNEL_PIPEUP_OUTPUT_DEPTH: if (size == sizeof(vx_uint32)) { *(vx_uint32 *)ptr = kernel->pipeup_output_depth; @@ -2565,6 +2570,7 @@ VX_API_ENTRY vx_status VX_API_CALL vxQueryKernel(vx_kernel kernel, vx_enum attri status = VX_SUCCESS; } break; +#endif default: status = VX_ERROR_NOT_SUPPORTED; break; @@ -2848,6 +2854,7 @@ VX_API_ENTRY vx_status VX_API_CALL vxSetKernelAttribute(vx_kernel kernel, vx_enu status = VX_SUCCESS; } break; +#if OPENVX_USE_PIPELINING case VX_KERNEL_PIPEUP_OUTPUT_DEPTH: if (size == sizeof(vx_uint32)) { vx_uint32 v = *(const vx_uint32 *)ptr; @@ -2862,6 +2869,7 @@ VX_API_ENTRY vx_status VX_API_CALL vxSetKernelAttribute(vx_kernel kernel, vx_enu else { kernel->pipeup_input_depth = v; status = VX_SUCCESS; } } break; +#endif case VX_KERNEL_ATTRIBUTE_AMD_NODE_REGEN_CALLBACK: if (size == sizeof(void *)) { if (!kernel->finalized) { @@ -3638,6 +3646,7 @@ VX_API_ENTRY vx_status VX_API_CALL vxQueryNode(vx_node node, vx_enum attribute, } break; #endif + default: status = VX_ERROR_NOT_SUPPORTED; break; } @@ -4601,7 +4610,11 @@ VX_API_ENTRY vx_status VX_API_CALL vxQueryReference(vx_reference ref, vx_enum at break; case VX_REFERENCE_ENQUEUE_COUNT: if (size == sizeof(vx_uint32)) { +#if OPENVX_USE_PIPELINING + *(vx_uint32 *)ptr = agoGetReferenceEnqueueCount(ref->context, ref); +#else *(vx_uint32 *)ptr = 0; +#endif status = VX_SUCCESS; } break; diff --git a/amd_openvx/openvx/api/vx_pipelining_api.cpp b/amd_openvx/openvx/api/vx_pipelining_api.cpp index 922b1d960..3e52c9d09 100644 --- a/amd_openvx/openvx/api/vx_pipelining_api.cpp +++ b/amd_openvx/openvx/api/vx_pipelining_api.cpp @@ -190,6 +190,10 @@ VX_API_ENTRY vx_status VX_API_CALL vxGraphParameterEnqueueReadyRef( std::lock_guard lock(q->mtx); q->ready_refs.push_back((AgoData *)refs[i]); } + { + std::lock_guard lock(pipe->enqueue_mtx); + pipe->enqueue_cv.notify_all(); + } status = VX_SUCCESS; } return status; @@ -217,9 +221,15 @@ VX_API_ENTRY vx_status VX_API_CALL vxGraphParameterDequeueDoneRef( return VX_ERROR_INVALID_PARAMETERS; std::unique_lock lock(q->mtx); - while (q->done_refs.empty()) { - q->done_cv.wait(lock); + if (pipe->timeout_ms == VX_TIMEOUT_WAIT_FOREVER) { + q->done_cv.wait(lock, [q]() { return !q->done_refs.empty(); }); + } else { + if (!q->done_cv.wait_for(lock, std::chrono::milliseconds(pipe->timeout_ms), + [q]() { return !q->done_refs.empty(); })) + return VX_FAILURE; } + if (q->done_refs.empty()) + return VX_FAILURE; vx_uint32 count = 0; while (count < max_refs && !q->done_refs.empty()) { refs[count] = (vx_reference)q->done_refs.front(); @@ -294,8 +304,12 @@ VX_API_ENTRY vx_status VX_API_CALL vxWaitEvent(vx_context context, vx_event_t *e if (evsys->events.empty()) return VX_FAILURE; } else { - while (evsys->events.empty()) { - evsys->events_cv.wait(lock); + if (evsys->timeout_ms == VX_TIMEOUT_WAIT_FOREVER) { + evsys->events_cv.wait(lock, [&evsys]() { return !evsys->events.empty(); }); + } else { + if (!evsys->events_cv.wait_for(lock, std::chrono::milliseconds(evsys->timeout_ms), + [&evsys]() { return !evsys->events.empty(); })) + return VX_FAILURE; } } if (evsys->events.empty()) From aabbffe38a03ceaa9efd3e04481c7c202e373f7b Mon Sep 17 00:00:00 2001 From: Kiriti Gowda Date: Tue, 28 Jul 2026 10:19:28 -0700 Subject: [PATCH 07/19] Update conformance-hip.yml --- .github/workflows/conformance-hip.yml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/workflows/conformance-hip.yml b/.github/workflows/conformance-hip.yml index 910255a3e..a3e7879ed 100644 --- a/.github/workflows/conformance-hip.yml +++ b/.github/workflows/conformance-hip.yml @@ -1033,9 +1033,9 @@ jobs: if-no-files-found: ignore pipelining-hip: - name: Pipelining (HIP) + name: KHR EXT: Pipelining (HIP) runs-on: - group: linux-shark42-runner-group + group: linux-shark39-runner-group container: image: mivisionx/ubuntu-24.04:rocm-20260710 options: --device /dev/kfd --device /dev/dri @@ -1074,9 +1074,9 @@ jobs: if-no-files-found: ignore streaming-hip: - name: Streaming (HIP) + name: KHR EXT: Streaming (HIP) runs-on: - group: linux-shark42-runner-group + group: linux-shark39-runner-group container: image: mivisionx/ubuntu-24.04:rocm-20260710 options: --device /dev/kfd --device /dev/dri From 00ff487b18cb2002dcc3c74f6b7a58360776bfdb Mon Sep 17 00:00:00 2001 From: Kiriti Gowda Date: Tue, 28 Jul 2026 10:27:53 -0700 Subject: [PATCH 08/19] Update conformance-hip.yml --- .github/workflows/conformance-hip.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/conformance-hip.yml b/.github/workflows/conformance-hip.yml index a3e7879ed..31348e9f6 100644 --- a/.github/workflows/conformance-hip.yml +++ b/.github/workflows/conformance-hip.yml @@ -1033,7 +1033,7 @@ jobs: if-no-files-found: ignore pipelining-hip: - name: KHR EXT: Pipelining (HIP) + name: KHR EXT - Pipelining (HIP) runs-on: group: linux-shark39-runner-group container: @@ -1074,7 +1074,7 @@ jobs: if-no-files-found: ignore streaming-hip: - name: KHR EXT: Streaming (HIP) + name: KHR EXT - Streaming (HIP) runs-on: group: linux-shark39-runner-group container: From e4963485f3fad92a62e3e278cb395e8b93227da5 Mon Sep 17 00:00:00 2001 From: simoncatbot Date: Tue, 28 Jul 2026 15:40:22 -0700 Subject: [PATCH 09/19] CI - update timeouts --- .github/workflows/conformance-hip.yml | 2 +- .github/workflows/conformance.yml | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/workflows/conformance-hip.yml b/.github/workflows/conformance-hip.yml index 31348e9f6..279075468 100644 --- a/.github/workflows/conformance-hip.yml +++ b/.github/workflows/conformance-hip.yml @@ -1061,7 +1061,7 @@ jobs: export LLVM_PROFILE_FILE=$GITHUB_WORKSPACE/profraw/pipelining_%p.profraw for target in CPU GPU; do echo "::group::pipelining (AGO_DEFAULT_TARGET=$target)" - AGO_DEFAULT_TARGET=$target timeout 5400 ./bin/vx_test_conformance --filter="GraphPipeline.*" + AGO_DEFAULT_TARGET=$target timeout 9000 ./bin/vx_test_conformance --filter="GraphPipeline.*" echo "::endgroup::" done - name: Upload profraw diff --git a/.github/workflows/conformance.yml b/.github/workflows/conformance.yml index 2aac6d736..e4ca942bd 100644 --- a/.github/workflows/conformance.yml +++ b/.github/workflows/conformance.yml @@ -639,7 +639,7 @@ jobs: if-no-files-found: ignore pipelining-cpu: - name: Pipelining (CPU) + name: KHR EXT - Pipelining (CPU) runs-on: ubuntu-24.04 needs: build-debug steps: @@ -656,7 +656,7 @@ jobs: export LD_LIBRARY_PATH=${{ github.workspace }}/install/lib:${{ github.workspace }}/OpenVX-cts/build/lib export VX_TEST_DATA_PATH=${{ github.workspace }}/OpenVX-cts/test_data/ export LLVM_PROFILE_FILE=${{ github.workspace }}/profraw/pipelining_%p.profraw - timeout 5400 ./bin/vx_test_conformance --filter="GraphPipeline.*" + timeout 9000 ./bin/vx_test_conformance --filter="GraphPipeline.*" - name: Upload profraw if: always() uses: actions/upload-artifact@v4 @@ -666,7 +666,7 @@ jobs: if-no-files-found: ignore streaming-cpu: - name: Streaming (CPU) + name: KHR EXT - Streaming (CPU) runs-on: ubuntu-24.04 needs: build-debug steps: From b6e945dbef096c894b66748725ee2844d0fa5edf Mon Sep 17 00:00:00 2001 From: KiritiGowda Date: Sun, 2 Aug 2026 14:43:38 -0700 Subject: [PATCH 10/19] fix(pipelining): repair graph schedule/wait handshake and Linux critical sections The GraphPipeline CTS job hung in ManualSchedule/loop_count=1000 on both the CPU and HIP runners, and once that was fixed the QUEUE_AUTO tests aborted intermittently with a glibc mutex-owner assertion. Two distinct causes: 1. vxWaitGraph handshake. agoWaitGraph set threadThreadWaitState to 1 and spun until the graph thread set it to 2, but the graph thread only does so after returning from WaitForSingleObject(hSemToThread). When the graph thread finished an execution before the application reached vxWaitGraph it was already parked back on the semaphore with nothing left to wake it, so the spin never terminated. develop hid this by re-posting hSemToThread from inside the spin, which this branch removed because those posts caused phantom executions. Replace the state polling with exact completion accounting: the graph thread posts one token per scheduled execution and the waiter claims one token per execution scheduled but not yet accounted for. This is correct no matter which thread runs first, and it also drops GraphPipeline.* from minutes to seconds because waits no longer poll at 1 ms. threadThreadWaitState is replaced by threadCompletionCount, and threadScheduleCount is now only incremented once the request is actually posted. 2. Linux critical sections. EnterCriticalSection took the mutex with a std::lock_guard local, so it released it again on return, and every matching LeaveCriticalSection then unlocked an already-unlocked mutex. That was survivable while nothing contended graph->cs, but the pipelining executor thread makes it genuinely contended and the stray unlocks corrupt the futex state. Hold the lock for real, using a recursive_mutex to match Win32 CRITICAL_SECTION semantics (agoProcessGraph -> vxVerifyGraph re-enters). With the locks working, three sites that join a worker thread from inside a section that worker needs would deadlock, so move those joins out: vxSetGraphScheduleConfig, vxStopGraphStreaming and agoReleaseGraph. The latter now also stops the pipelining executor before the graph is dismantled, closing a window where the executor kept running over a torn-down graph. Also enable OPENVX_USE_STREAMING when configuring the CTS. Without it the streaming tests are compiled out, so the KHR EXT - Streaming jobs were passing vacuously on zero tests. Lower the pipelining timeout from 9000s to 1800s so a future hang fails in 30 minutes instead of 2.5 hours. Verification (CPU backend, CTS openvx_1.3.2): - GraphPipeline.* x5 Release: 109/109 each - GraphStreaming.* x3: 24/24 each - Full CTS x3 Release: 5952/5957 each, identical - Debug build over the CI suite filters: no failures - runvx GDF and vision node tests: pass The 5 remaining Release failures are Array.vxCreateArray; pristine develop fails the same 5 identically and they pass in the Debug build CI uses. Co-authored-by: Cursor --- .github/workflows/conformance-hip.yml | 3 +- .github/workflows/conformance.yml | 5 +- amd_openvx/openvx/ago/ago_interface.cpp | 52 ++++++++++++++------- amd_openvx/openvx/ago/ago_internal.h | 5 +- amd_openvx/openvx/ago/ago_platform.cpp | 6 ++- amd_openvx/openvx/ago/ago_util.cpp | 2 +- amd_openvx/openvx/api/vx_pipelining_api.cpp | 11 +++-- 7 files changed, 57 insertions(+), 27 deletions(-) diff --git a/.github/workflows/conformance-hip.yml b/.github/workflows/conformance-hip.yml index 279075468..3f7378840 100644 --- a/.github/workflows/conformance-hip.yml +++ b/.github/workflows/conformance-hip.yml @@ -285,6 +285,7 @@ jobs: -DOPENVX_LIBRARIES="$GITHUB_WORKSPACE/install/lib/libopenvx.so;$GITHUB_WORKSPACE/install/lib/libvxu.so;pthread;dl;m;rt" \ -DOPENVX_CONFORMANCE_VISION=ON \ -DOPENVX_USE_PIPELINING=ON \ + -DOPENVX_USE_STREAMING=ON \ -DCMAKE_C_STANDARD_LIBRARIES="-L${ROCM_PATH}/lib -lamdhip64" \ -DCMAKE_CXX_STANDARD_LIBRARIES="-L${ROCM_PATH}/lib -lamdhip64" \ -DCMAKE_EXE_LINKER_FLAGS="-Wl,-rpath-link,${ROCM_PATH}/lib" @@ -1061,7 +1062,7 @@ jobs: export LLVM_PROFILE_FILE=$GITHUB_WORKSPACE/profraw/pipelining_%p.profraw for target in CPU GPU; do echo "::group::pipelining (AGO_DEFAULT_TARGET=$target)" - AGO_DEFAULT_TARGET=$target timeout 9000 ./bin/vx_test_conformance --filter="GraphPipeline.*" + AGO_DEFAULT_TARGET=$target timeout 1800 ./bin/vx_test_conformance --filter="GraphPipeline.*" echo "::endgroup::" done - name: Upload profraw diff --git a/.github/workflows/conformance.yml b/.github/workflows/conformance.yml index e4ca942bd..57770f309 100644 --- a/.github/workflows/conformance.yml +++ b/.github/workflows/conformance.yml @@ -159,7 +159,8 @@ jobs: -DOPENVX_INCLUDES="${{ github.workspace }}/amd_openvx/openvx/include" \ -DOPENVX_LIBRARIES="${INSTALL_PREFIX}/lib/libopenvx.so;${INSTALL_PREFIX}/lib/libvxu.so;pthread;dl;m;rt" \ -DOPENVX_CONFORMANCE_VISION=ON \ - -DOPENVX_USE_PIPELINING=ON + -DOPENVX_USE_PIPELINING=ON \ + -DOPENVX_USE_STREAMING=ON make -j$(nproc) - name: Upload debug build artifacts @@ -656,7 +657,7 @@ jobs: export LD_LIBRARY_PATH=${{ github.workspace }}/install/lib:${{ github.workspace }}/OpenVX-cts/build/lib export VX_TEST_DATA_PATH=${{ github.workspace }}/OpenVX-cts/test_data/ export LLVM_PROFILE_FILE=${{ github.workspace }}/profraw/pipelining_%p.profraw - timeout 9000 ./bin/vx_test_conformance --filter="GraphPipeline.*" + timeout 1800 ./bin/vx_test_conformance --filter="GraphPipeline.*" - name: Upload profraw if: always() uses: actions/upload-artifact@v4 diff --git a/amd_openvx/openvx/ago/ago_interface.cpp b/amd_openvx/openvx/ago/ago_interface.cpp index 26a642d88..1bae77cec 100644 --- a/amd_openvx/openvx/ago/ago_interface.cpp +++ b/amd_openvx/openvx/ago/ago_interface.cpp @@ -33,7 +33,6 @@ static void agoGraphThreadFunction(LPVOID graph_) AgoGraph * graph = (AgoGraph *)graph_; for (;;) { DWORD w = WaitForSingleObject(graph->hSemToThread, INFINITE); - graph->threadThreadWaitState.store(2); if (graph->threadThreadTerminationState.load()) break; // Ignore spurious/error returns from the semaphore wait and retry. @@ -163,8 +162,32 @@ AgoGraph * agoCreateGraph(AgoContext * acontext) return (AgoGraph *)agraph; } +// Signals the graph scheduling thread to exit and waits for it to do so. Must be called +// without holding the graph or context critical section: the thread executes the graph +// inside both of them, so joining it from within either one deadlocks. +static void agoStopGraphThread(AgoGraph * agraph) +{ + if (!agraph->hThread || agraph->threadThreadTerminationState.load() == 2) + return; + agraph->threadThreadTerminationState.store(1); + ReleaseSemaphore(agraph->hSemToThread, 1, nullptr); + while (agraph->threadThreadTerminationState.load() == 1) { + // give a chance for the thread to run in case it is waititng + std::this_thread::sleep_for(std::chrono::milliseconds(1)); + ReleaseSemaphore(agraph->hSemToThread, 1, nullptr); + } +} + int agoReleaseGraph(AgoGraph * agraph) { + // Stop every thread that executes this graph before taking any lock, so that the + // graph is not being torn down underneath them and so that joining them cannot + // deadlock against a section they are trying to enter. + if (agraph->ref.external_count <= 1) { + agoStopGraphPipelining(agraph); + agoStopGraphThread(agraph); + } + CAgoLock lock(agraph->ref.context->cs); int status = 0; @@ -173,15 +196,7 @@ int agoReleaseGraph(AgoGraph * agraph) agraph->ref.context->num_active_references--; if (agraph->ref.external_count == 0) { EnterCriticalSection(&agraph->cs); - // stop graph thread if (agraph->hThread) { - agraph->threadThreadTerminationState.store(1); - ReleaseSemaphore(agraph->hSemToThread, 1, nullptr); - while (agraph->threadThreadTerminationState.load() == 1) { - // give a chance for the thread to run in case it is waititng - std::this_thread::sleep_for(std::chrono::milliseconds(1)); - ReleaseSemaphore(agraph->hSemToThread, 1, nullptr); - } if (agraph->hSemToThread) { CloseHandle(agraph->hSemToThread); } @@ -2955,7 +2970,6 @@ int agoScheduleGraph(AgoGraph * graph) vx_status status = VX_ERROR_INVALID_REFERENCE; if (agoIsValidGraph(graph)) { status = VX_SUCCESS; - graph->threadScheduleCount.fetch_add(1); if (graph->hThread) { if (!graph->verified) { // make sure to verify the graph in master thread @@ -2963,13 +2977,18 @@ int agoScheduleGraph(AgoGraph * graph) status = vxVerifyGraph(graph); } if (status == VX_SUCCESS) { + // count the request before waking the graph thread, so a waiter can + // never observe the completion token without the matching request + graph->threadScheduleCount.fetch_add(1); // inform graph thread to execute if (!ReleaseSemaphore(graph->hSemToThread, 1, nullptr)) { + graph->threadScheduleCount.fetch_sub(1); status = VX_ERROR_NO_RESOURCES; } } } else { + graph->threadScheduleCount.fetch_add(1); status = agoProcessGraph(graph); } } @@ -3053,17 +3072,18 @@ int agoWaitGraph(AgoGraph * graph) if (graph->threadScheduleCount.load() <= 0) // the graph was never scheduled so return VX_FAILURE return VX_FAILURE; if (graph->hThread) { - graph->threadThreadWaitState.store(1); - while (graph->threadThreadWaitState.load() == 1) { - // wait for the agoGraphThreadFunction to be done - std::this_thread::sleep_for(std::chrono::milliseconds(1)); - } - while (graph->threadExecuteCount.load() < graph->threadScheduleCount.load()) { + // The graph thread posts exactly one completion token per scheduled execution, + // so claim a token for every execution scheduled so far that no earlier wait has + // accounted for. Counting tokens rather than polling thread state keeps this + // correct no matter whether the graph thread runs before or after this call. + vx_int32 target = graph->threadScheduleCount.load(); + while (graph->threadCompletionCount.load() < target) { if (WaitForSingleObject(graph->hSemFromThread, INFINITE) != WAIT_OBJECT_0) { agoAddLogEntry(&graph->ref, VX_FAILURE, "ERROR: agoWaitGraph: WaitForSingleObject failed\n"); status = VX_FAILURE; break; } + graph->threadCompletionCount.fetch_add(1); } } if(status == VX_SUCCESS) diff --git a/amd_openvx/openvx/ago/ago_internal.h b/amd_openvx/openvx/ago/ago_internal.h index 9d9c301d4..24f64abaa 100644 --- a/amd_openvx/openvx/ago/ago_internal.h +++ b/amd_openvx/openvx/ago/ago_internal.h @@ -741,7 +741,10 @@ struct AgoGraph { AgoGraph * next; CRITICAL_SECTION cs; HANDLE hThread, hSemToThread, hSemFromThread; - std::atomic threadScheduleCount, threadExecuteCount, threadWaitCount, threadThreadTerminationState, threadThreadWaitState; + // threadScheduleCount counts executions handed to the graph thread, threadExecuteCount + // counts executions it finished, and threadCompletionCount counts the completions already + // claimed by a waiter -- one completion token on hSemFromThread per scheduled execution. + std::atomic threadScheduleCount, threadExecuteCount, threadCompletionCount, threadWaitCount, threadThreadTerminationState; AgoDataList dataList; AgoNodeList nodeList; vx_bool isReadyToExecute; diff --git a/amd_openvx/openvx/ago/ago_platform.cpp b/amd_openvx/openvx/ago/ago_platform.cpp index 1cfd27e7d..461506c3c 100644 --- a/amd_openvx/openvx/ago/ago_platform.cpp +++ b/amd_openvx/openvx/ago/ago_platform.cpp @@ -281,7 +281,9 @@ typedef struct { typedef struct { int type; // should be VX_CRITICAL_SECTION - mutex mtx; + // recursive to match Win32 CRITICAL_SECTION semantics: several call paths + // re-enter the same section, e.g. agoProcessGraph -> vxVerifyGraph + recursive_mutex mtx; } vx_critical_section; @@ -289,7 +291,7 @@ typedef struct { void EnterCriticalSection(CRITICAL_SECTION* cs) { vx_critical_section * crit_sec = (vx_critical_section *)*cs; - std::lock_guard lock(crit_sec->mtx); + crit_sec->mtx.lock(); } // Emulates LeaveCriticalSection for non_windows platform diff --git a/amd_openvx/openvx/ago/ago_util.cpp b/amd_openvx/openvx/ago/ago_util.cpp index 102794b6f..27763e10d 100644 --- a/amd_openvx/openvx/ago/ago_util.cpp +++ b/amd_openvx/openvx/ago/ago_util.cpp @@ -3423,7 +3423,7 @@ AgoNode::~AgoNode() } AgoGraph::AgoGraph() : next{ nullptr }, hThread{ nullptr }, hSemToThread{ nullptr }, hSemFromThread{ nullptr }, - threadScheduleCount{ 0 }, threadExecuteCount{ 0 }, threadWaitCount{ 0 }, threadThreadTerminationState{ 0 }, threadThreadWaitState{ 0 }, + threadScheduleCount{ 0 }, threadExecuteCount{ 0 }, threadCompletionCount{ 0 }, threadWaitCount{ 0 }, threadThreadTerminationState{ 0 }, isReadyToExecute{ vx_false_e }, detectedInvalidNode{ false }, status{ VX_SUCCESS }, virtualDataGenerationCount{ 0 }, optimizer_flags{ AGO_GRAPH_OPTIMIZER_FLAGS_DEFAULT }, verified{ false }, enable_performance_profiling{ false }, execFrameCount{ 0 }, pipelining{ nullptr } #if ENABLE_OPENCL diff --git a/amd_openvx/openvx/api/vx_pipelining_api.cpp b/amd_openvx/openvx/api/vx_pipelining_api.cpp index 3e52c9d09..bae0ab532 100644 --- a/amd_openvx/openvx/api/vx_pipelining_api.cpp +++ b/amd_openvx/openvx/api/vx_pipelining_api.cpp @@ -55,11 +55,12 @@ VX_API_ENTRY vx_status VX_API_CALL vxSetGraphScheduleConfig( if (!pipe) return VX_FAILURE; - CAgoLock lock(graph->cs); - - // Stop any active executor before reconfiguring. + // Stop any active executor before reconfiguring. This has to happen outside + // graph->cs because the executor runs the graph inside that section. agoStopGraphPipelining(graph); + CAgoLock lock(graph->cs); + pipe->schedule_mode = graph_schedule_mode; pipe->param_queues.clear(); pipe->param_queues.resize(graph->parameters.size()); @@ -481,11 +482,13 @@ VX_API_ENTRY vx_status VX_API_CALL vxStopGraphStreaming(vx_graph graph) AgoGraphPipeliningState * pipe = agoGetGraphPipeliningState(graph); if (!pipe) return VX_FAILURE; - CAgoLock lock(graph->cs); + // The streaming thread executes the graph under graph->cs, so it has to be + // joined before that section is entered. pipe->streaming_stop.store(true); if (pipe->streaming_thread.joinable()) { pipe->streaming_thread.join(); } + CAgoLock lock(graph->cs); pipe->streaming_enabled = false; status = VX_SUCCESS; } From 68ba220a9d0e15c73d0cdd72e403e6d62b741b91 Mon Sep 17 00:00:00 2001 From: KiritiGowda Date: Mon, 3 Aug 2026 08:29:41 -0700 Subject: [PATCH 11/19] fix(pipelining): give queued graph-parameter refs device memory on GPU GraphPipeline.* failed 90 of 109 tests on the HIP GPU target: outputs came back all zeros and the first kernel launch raised hipErrorIllegalAddress (700), which poisons the HIP context so every later test failed at vxVerifyGraph. The same 109 tests passed on the CPU target. Graph verification reserves a device buffer for each data object bound to a GPU node parameter at the time it runs. References supplied through vxGraphParameterEnqueueReadyRef arrive afterwards and are substituted straight into node->paramList, so they reached the executor with hip_memory == nullptr. agoGpuHipDataInputSync skips the host-to-device copy when that pointer is null rather than reporting an error, so the kernel launched against null and the host buffer the application later maps was never written. Reserve buffers for the newly bound references once substitution is complete, using the same rules verification applies, so the existing sync machinery can move the data in both directions. The allocation only happens the first time a reference is used, and CPU-only builds compile it out. Also pack the HIP debug CI artifact as a tarball. upload-artifact cannot represent symlinks, so it expanded libopenvx.so/.so.1/.so.1.3.2 into three identical copies (same for libvxu) - 74 MB of duplication in a 97 MB artifact spread over 1801 files, which each of the 17 consuming jobs refetched. Vision pyramid (HIP) failed because its download was cut off at exactly 240s with no completion line, leaving test_data short of optflow_00.bmp and surfacing as four bogus OptFlowPyrLK failures. A tarball keeps the symlinks, makes the transfer a single stream, and turns a truncated download into a loud tar failure instead of a phantom test failure. Verified on gfx1100 against the full CTS, all 13 suites, on three configurations (HIP/GPU, HIP/CPU, CPU-only): zero failures in each. GraphPipeline on HIP/GPU went from 19/109 to 109/109 and held across four consecutive runs. Co-authored-by: Cursor --- .github/workflows/conformance-hip.yml | 67 ++++++++++++++++++++---- amd_openvx/openvx/ago/ago_pipelining.cpp | 57 +++++++++++++++++++- 2 files changed, 111 insertions(+), 13 deletions(-) diff --git a/.github/workflows/conformance-hip.yml b/.github/workflows/conformance-hip.yml index 3f7378840..cdef00320 100644 --- a/.github/workflows/conformance-hip.yml +++ b/.github/workflows/conformance-hip.yml @@ -302,21 +302,31 @@ jobs: strip --strip-debug OpenVX-cts/build/bin/vx_test_conformance 2>/dev/null || true strip --strip-debug OpenVX-cts/build/lib/*.so* 2>/dev/null || true + - name: Pack test bundle + run: | + # Ship one tarball rather than the raw tree. upload-artifact cannot + # represent symlinks, so it would expand libopenvx.so, .so.1 and + # .so.1.3.2 into three identical copies (same for libvxu) — ~74 MB of + # duplication on its own, and ~1800 individual files for each of the + # 16 consuming jobs to fetch and write. tar keeps the symlinks and + # turns the download into a single stream. It also makes truncation + # loud: a short tarball fails to extract instead of silently leaving + # part of test_data missing and surfacing as a bogus test failure. + tar -czf test-bundle-hip.tar.gz \ + install/ \ + build/api_tests/ \ + OpenVX-cts/build/bin/vx_test_conformance \ + OpenVX-cts/build/lib/ \ + OpenVX-cts/test_data/ + ls -lh test-bundle-hip.tar.gz + - name: Upload debug build artifacts uses: actions/upload-artifact@v4 with: name: build-artifacts-hip-debug - # Upload the full install tree: the libs (lib/), runvx (bin/) and the - # sample GDFs + data under share/ are all used by various test jobs. - # The artifact size is already controlled by the two-arch GPU_TARGETS - # build and the debug-symbol strip above (device code + DWARF were the - # bulk); include/ and share/ are small by comparison. - path: | - install/ - build/api_tests/ - OpenVX-cts/build/bin/vx_test_conformance - OpenVX-cts/build/lib/ - OpenVX-cts/test_data/ + path: test-bundle-hip.tar.gz + # Already gzipped; re-compressing in the zip only costs time. + compression-level: 0 retention-days: 1 # =========================================================================== @@ -529,6 +539,8 @@ jobs: uses: actions/download-artifact@v4 with: name: build-artifacts-hip-debug + - name: Unpack test bundle + run: tar -xzf test-bundle-hip.tar.gz - name: Validate ROCm run: | rocminfo @@ -577,6 +589,8 @@ jobs: uses: actions/download-artifact@v4 with: name: build-artifacts-hip-debug + - name: Unpack test bundle + run: tar -xzf test-bundle-hip.tar.gz - name: Validate ROCm run: | rocminfo @@ -624,6 +638,8 @@ jobs: uses: actions/download-artifact@v4 with: name: build-artifacts-hip-debug + - name: Unpack test bundle + run: tar -xzf test-bundle-hip.tar.gz - name: Validate ROCm run: | rocminfo @@ -671,6 +687,8 @@ jobs: uses: actions/download-artifact@v4 with: name: build-artifacts-hip-debug + - name: Unpack test bundle + run: tar -xzf test-bundle-hip.tar.gz - name: Validate ROCm run: | rocminfo @@ -718,6 +736,8 @@ jobs: uses: actions/download-artifact@v4 with: name: build-artifacts-hip-debug + - name: Unpack test bundle + run: tar -xzf test-bundle-hip.tar.gz - name: Validate ROCm run: | rocminfo @@ -765,6 +785,8 @@ jobs: uses: actions/download-artifact@v4 with: name: build-artifacts-hip-debug + - name: Unpack test bundle + run: tar -xzf test-bundle-hip.tar.gz - name: Validate ROCm run: | rocminfo @@ -812,6 +834,8 @@ jobs: uses: actions/download-artifact@v4 with: name: build-artifacts-hip-debug + - name: Unpack test bundle + run: tar -xzf test-bundle-hip.tar.gz - name: Validate ROCm run: | rocminfo @@ -859,6 +883,8 @@ jobs: uses: actions/download-artifact@v4 with: name: build-artifacts-hip-debug + - name: Unpack test bundle + run: tar -xzf test-bundle-hip.tar.gz - name: Validate ROCm run: | rocminfo @@ -906,6 +932,8 @@ jobs: uses: actions/download-artifact@v4 with: name: build-artifacts-hip-debug + - name: Unpack test bundle + run: tar -xzf test-bundle-hip.tar.gz - name: Validate ROCm run: | rocminfo @@ -953,6 +981,8 @@ jobs: uses: actions/download-artifact@v4 with: name: build-artifacts-hip-debug + - name: Unpack test bundle + run: tar -xzf test-bundle-hip.tar.gz - name: Validate ROCm run: | rocminfo @@ -1000,6 +1030,8 @@ jobs: uses: actions/download-artifact@v4 with: name: build-artifacts-hip-debug + - name: Unpack test bundle + run: tar -xzf test-bundle-hip.tar.gz - name: Validate ROCm run: | rocminfo @@ -1046,6 +1078,8 @@ jobs: uses: actions/download-artifact@v4 with: name: build-artifacts-hip-debug + - name: Unpack test bundle + run: tar -xzf test-bundle-hip.tar.gz - name: Validate ROCm run: | rocminfo @@ -1087,6 +1121,8 @@ jobs: uses: actions/download-artifact@v4 with: name: build-artifacts-hip-debug + - name: Unpack test bundle + run: tar -xzf test-bundle-hip.tar.gz - name: Validate ROCm run: | rocminfo @@ -1131,6 +1167,8 @@ jobs: uses: actions/download-artifact@v4 with: name: build-artifacts-hip-debug + - name: Unpack test bundle + run: tar -xzf test-bundle-hip.tar.gz - name: Validate ROCm run: | rocminfo @@ -1208,6 +1246,8 @@ jobs: uses: actions/download-artifact@v4 with: name: build-artifacts-hip-debug + - name: Unpack test bundle + run: tar -xzf test-bundle-hip.tar.gz - name: Install python3 run: | apt-get update -qq @@ -1264,6 +1304,8 @@ jobs: uses: actions/download-artifact@v4 with: name: build-artifacts-hip-debug + - name: Unpack test bundle + run: tar -xzf test-bundle-hip.tar.gz - name: Install python3 run: | apt-get update -qq @@ -1626,6 +1668,9 @@ jobs: with: name: build-artifacts-hip-debug + - name: Unpack test bundle + run: tar -xzf test-bundle-hip.tar.gz + - name: Download all HIP profraw artifacts uses: actions/download-artifact@v4 with: diff --git a/amd_openvx/openvx/ago/ago_pipelining.cpp b/amd_openvx/openvx/ago/ago_pipelining.cpp index e1219172b..2ab4c9b5d 100644 --- a/amd_openvx/openvx/ago/ago_pipelining.cpp +++ b/amd_openvx/openvx/ago/ago_pipelining.cpp @@ -368,6 +368,55 @@ static void agoApplyDataRefSwapWithSiblings(AgoGraph * graph, AgoData * original } } +// Graph verification reserves a device buffer for every data object that was +// bound to a GPU node parameter at verification time. Queued references come +// from the application afterwards, so once they are substituted into the graph +// they still have none and the node would launch against a null device +// pointer. Reserve buffers for the newly bound references using the same rules +// verification applies, after all substitutions are in place. +static int agoAllocGpuBuffersForQueuedRefs(AgoGraph * graph) +{ +#if (ENABLE_OPENCL||ENABLE_HIP) + for (AgoNode * node = graph->nodeList.head; node; node = node->next) { + if (node->attr_affinity.device_type != AGO_KERNEL_FLAG_DEVICE_GPU && + !node->akernel->opencl_buffer_access_enable) + continue; + for (vx_uint32 i = 0; i < node->paramCount; i++) { + AgoData * data = node->paramList[i]; + if (!data || data->isVirtual) + continue; +#if ENABLE_OPENCL + if (data->opencl_buffer) + continue; +#else + if (data->hip_memory) + continue; +#endif + if (agoIsPartOfDelay(data)) { + int siblingTrace[AGO_MAX_DEPTH_FROM_DELAY_OBJECT], siblingTraceCount = 0; + data = agoGetSiblingTraceToDelayForUpdate(data, siblingTrace, siblingTraceCount); + if (!data) + return VX_FAILURE; + } +#if ENABLE_OPENCL + if (agoGpuOclAllocBuffer(data) < 0) +#else + if (agoGpuHipAllocBuffer(data) < 0) +#endif + { + agoAddLogEntry(&graph->ref, VX_FAILURE, + "ERROR: agoAllocGpuBuffersForQueuedRefs: GPU buffer allocation failed for node %s arg#%d\n", + node->akernel->name, i); + return VX_FAILURE; + } + } + } +#else + (void)graph; +#endif + return VX_SUCCESS; +} + static std::vector agoCollectGraphParameterBindings(AgoGraph * graph) { std::vector bindings; @@ -467,8 +516,12 @@ int agoExecutePipelinedGraphOnce(AgoGraph * graph) std::vector consumed_refs; agoApplyQueuedRefsToBindings(graph, pipe, bindings, consumed_refs); - // Execute the graph synchronously using the normal path. - int status = agoExecuteGraph(graph); + // Back the newly substituted references with device memory, then execute + // the graph synchronously using the normal path. + int status = agoAllocGpuBuffersForQueuedRefs(graph); + if (status == VX_SUCCESS) { + status = agoExecuteGraph(graph); + } // Restore original bindings so the next execution sees the static defaults. agoRestoreBindings(graph, bindings); From 908b50adda7c2779bd7fedbdecf52175ed4e931f Mon Sep 17 00:00:00 2001 From: KiritiGowda Date: Mon, 3 Aug 2026 10:42:22 -0700 Subject: [PATCH 12/19] fix(pipelining): only report registered events, and validate registration The event queue reported events the application never asked for and dropped information it did ask for. The conformance suite does not catch any of this because its event test dispatches on app_value through an if/else chain and silently ignores anything it does not recognise. Measured with a probe that registers NODE_COMPLETED on one of two nodes and runs a single execution: six events arrived where three were due. One was NODE_COMPLETED for an internal node with app_value 0, another was GRAPH_PARAMETER_CONSUMED for a parameter that was never registered. Event generation is now gated on there being a matching registration, so the lookup has to report whether one exists rather than handing back an app_value that defaults to zero. The post-graph sweep that covers nodes rewritten away by optimization now skips nodes still present in nodeList, since those already reported through the executor and would otherwise report twice. GRAPH_PARAMETER_CONSUMED fires once per reference actually consumed; keying it off done_refs meant it kept firing for as long as the application left anything undequeued, including for parameters that consumed nothing. vxRegisterEvent now enforces what the spec asks of it: - the event type must be valid for the kind of reference given, so NODE_COMPLETED on a graph and GRAPH_COMPLETED on a node are rejected with VX_ERROR_NOT_SUPPORTED instead of silently accepted - registration must happen before vxVerifyGraph - the parameter index is range-checked for GRAPH_PARAMETER_CONSUMED - re-registering the same reference, type and parameter updates the stored app_value rather than appending a second registration that the lookup then shadowed, which had made the update silently ineffective Also: vxSendUserEvent stamps a real timestamp instead of 0; a blocking vxWaitEvent stays blocked while events are disabled; vxGraphParameterEnqueue- ReadyRef enforces the configured refs_list_size and returns VX_ERROR_NO_RESOURCES past it; QUEUE_MANUAL reports failure from vxScheduleGraph when no graph parameter set was ready instead of returning success having done nothing; vxSetGraphScheduleConfig rejects a null refs_list as the spec requires it to; and vxGetGraphParameterRefsList and vxAddReferencesToGraphParameterList return VX_ERROR_INVALID_GRAPH rather than VX_ERROR_INVALID_REFERENCE for an invalid or unverified graph. Two rules were deliberately left unenforced because conformance depends on it. GraphPipeline.ScalarOutput configures graph parameter 1 twice and never configures parameter 2, so the spec's requirement that graph_parameter_index be unique cannot be checked, and the queue for parameter 2 has to keep being auto-enabled on first enqueue. Those auto-enabled queues are left unbounded rather than inferring a depth from the first enqueue call. Verified on gfx1100 against the full CTS on three configurations (HIP/GPU, HIP/CPU, CPU-only): 5957 tests, zero failures each, with pipelining 109/109 and streaming 24/24. The probe now reports exactly the three registered events for one execution, and app_value updates on re-registration. Co-authored-by: Cursor --- amd_openvx/openvx/ago/ago_internal.h | 1 + amd_openvx/openvx/ago/ago_pipelining.cpp | 87 +++++++++++++--- amd_openvx/openvx/api/vx_pipelining_api.cpp | 107 +++++++++++++++----- 3 files changed, 156 insertions(+), 39 deletions(-) diff --git a/amd_openvx/openvx/ago/ago_internal.h b/amd_openvx/openvx/ago/ago_internal.h index 24f64abaa..75ea77449 100644 --- a/amd_openvx/openvx/ago/ago_internal.h +++ b/amd_openvx/openvx/ago/ago_internal.h @@ -1002,6 +1002,7 @@ void agoNotifyGraphParameterConsumed(AgoGraph * graph, vx_uint32 graph_parameter bool agoGraphHasNodeEventRegistrations(AgoGraph * graph); vx_uint32 agoGetReferenceEnqueueCount(AgoContext * context, AgoReference * ref); void agoRemoveEventRegistrations(AgoContext * context, vx_reference ref); +vx_uint64 agoEventTimestampNs(); #if (ENABLE_OPENCL || ENABLE_HIP) int agoGpuOclAllocBuffers(AgoGraph * graph); diff --git a/amd_openvx/openvx/ago/ago_pipelining.cpp b/amd_openvx/openvx/ago/ago_pipelining.cpp index 2ab4c9b5d..e55e5eff9 100644 --- a/amd_openvx/openvx/ago/ago_pipelining.cpp +++ b/amd_openvx/openvx/ago/ago_pipelining.cpp @@ -123,19 +123,30 @@ static AgoGraphParameterQueue * agoGetGraphParameterQueue(AgoGraphPipeliningStat // -static vx_uint32 agoFindEventAppValue(AgoContext * context, vx_reference ref, vx_enum event_type, vx_uint32 graph_parameter_index) +// The framework only reports events for references the application asked about +// through vxRegisterEvent, so the lookup has to say whether a registration +// exists at all -- not just hand back an app_value that defaults to zero. +static bool agoFindEventRegistration(AgoContext * context, vx_reference ref, vx_enum event_type, + vx_uint32 graph_parameter_index, vx_uint32 * app_value) { AgoContextEventSystem * evsys = agoGetContextEventSystem(context); if (!evsys) - return 0; + return false; std::lock_guard lock(evsys->registrations_mtx); for (const auto& reg : evsys->registrations) { if (reg.ref == ref && reg.event_type == event_type && (event_type != VX_EVENT_GRAPH_PARAMETER_CONSUMED || reg.graph_parameter_index == graph_parameter_index)) { - return reg.app_value; + if (app_value) + *app_value = reg.app_value; + return true; } } - return 0; + return false; +} + +vx_uint64 agoEventTimestampNs() +{ + return agoCurrentTimestampNs(); } void agoRemoveEventRegistrations(AgoContext * context, vx_reference ref) @@ -202,10 +213,13 @@ void agoNotifyGraphCompleted(AgoGraph * graph) AgoContextEventSystem * evsys = agoGetContextEventSystem(graph->ref.context); if (!evsys || !evsys->enabled) return; + vx_uint32 app_value = 0; + if (!agoFindEventRegistration(graph->ref.context, (vx_reference)graph, VX_EVENT_GRAPH_COMPLETED, 0, &app_value)) + return; AgoEvent evt; evt.event_type = VX_EVENT_GRAPH_COMPLETED; evt.timestamp = agoCurrentTimestampNs(); - evt.app_value = agoFindEventAppValue(graph->ref.context, (vx_reference)graph, VX_EVENT_GRAPH_COMPLETED, 0); + evt.app_value = app_value; evt.graph = graph; evt.node = nullptr; evt.graph_parameter_index = 0; @@ -221,10 +235,13 @@ void agoNotifyNodeCompleted(AgoGraph * graph, AgoNode * node) AgoContextEventSystem * evsys = agoGetContextEventSystem(graph->ref.context); if (!evsys || !evsys->enabled) return; + vx_uint32 app_value = 0; + if (!agoFindEventRegistration(graph->ref.context, (vx_reference)node, VX_EVENT_NODE_COMPLETED, 0, &app_value)) + return; AgoEvent evt; evt.event_type = VX_EVENT_NODE_COMPLETED; evt.timestamp = agoCurrentTimestampNs(); - evt.app_value = agoFindEventAppValue(graph->ref.context, (vx_reference)node, VX_EVENT_NODE_COMPLETED, 0); + evt.app_value = app_value; evt.graph = graph; evt.node = node; evt.graph_parameter_index = 0; @@ -240,10 +257,13 @@ void agoNotifyNodeError(AgoGraph * graph, AgoNode * node, vx_status status) AgoContextEventSystem * evsys = agoGetContextEventSystem(graph->ref.context); if (!evsys || !evsys->enabled) return; + vx_uint32 app_value = 0; + if (!agoFindEventRegistration(graph->ref.context, (vx_reference)node, VX_EVENT_NODE_ERROR, 0, &app_value)) + return; AgoEvent evt; evt.event_type = VX_EVENT_NODE_ERROR; evt.timestamp = agoCurrentTimestampNs(); - evt.app_value = agoFindEventAppValue(graph->ref.context, (vx_reference)node, VX_EVENT_NODE_ERROR, 0); + evt.app_value = app_value; evt.graph = graph; evt.node = node; evt.graph_parameter_index = 0; @@ -252,6 +272,20 @@ void agoNotifyNodeError(AgoGraph * graph, AgoNode * node, vx_status status) agoInternalPushEvent(graph->ref.context, evt); } +// Nodes that survived graph optimization report completion from the executor +// itself. This covers the ones that did not: optimization can rewrite the +// user-visible nodes into internal ones, and the application still registered +// against the originals. Nodes still present in nodeList are skipped here +// because they have already reported, otherwise they would report twice. +static bool agoIsNodeInGraph(AgoGraph * graph, AgoNode * node) +{ + for (AgoNode * n = graph->nodeList.head; n; n = n->next) { + if (n == node) + return true; + } + return false; +} + static void agoEmitRegisteredNodeEvents(AgoGraph * graph, vx_enum event_type, vx_status err_status) { if (!graph || !graph->ref.context) @@ -266,6 +300,8 @@ static void agoEmitRegisteredNodeEvents(AgoGraph * graph, vx_enum event_type, vx AgoReference * r = (AgoReference *)reg.ref; if (!r || r->type != VX_TYPE_NODE || r->scope != (vx_reference)graph) continue; + if (agoIsNodeInGraph(graph, (AgoNode *)r)) + continue; AgoEvent evt; evt.event_type = event_type; evt.timestamp = agoCurrentTimestampNs(); @@ -286,10 +322,14 @@ void agoNotifyGraphParameterConsumed(AgoGraph * graph, vx_uint32 graph_parameter AgoContextEventSystem * evsys = agoGetContextEventSystem(graph->ref.context); if (!evsys || !evsys->enabled) return; + vx_uint32 app_value = 0; + if (!agoFindEventRegistration(graph->ref.context, (vx_reference)graph, VX_EVENT_GRAPH_PARAMETER_CONSUMED, + graph_parameter_index, &app_value)) + return; AgoEvent evt; evt.event_type = VX_EVENT_GRAPH_PARAMETER_CONSUMED; evt.timestamp = agoCurrentTimestampNs(); - evt.app_value = agoFindEventAppValue(graph->ref.context, (vx_reference)graph, VX_EVENT_GRAPH_PARAMETER_CONSUMED, graph_parameter_index); + evt.app_value = app_value; evt.graph = graph; evt.node = nullptr; evt.graph_parameter_index = graph_parameter_index; @@ -485,14 +525,20 @@ static void agoMoveConsumedRefsToDone(AgoGraph * graph) if (!pipe) return; for (auto& q : pipe->param_queues) { - std::lock_guard lock(q->mtx); - - while (!q->consumed_refs.empty()) { - q->done_refs.push_back(q->consumed_refs.front()); - q->consumed_refs.pop_front(); + vx_uint32 moved = 0; + { + std::lock_guard lock(q->mtx); + while (!q->consumed_refs.empty()) { + q->done_refs.push_back(q->consumed_refs.front()); + q->consumed_refs.pop_front(); + moved++; + } } - if (!q->done_refs.empty()) { - // Notify that a reference at this parameter was consumed during this execution. + // One event per reference actually consumed by this execution. Keying off + // done_refs instead would keep reporting for as long as the application + // leaves anything undequeued, including for parameters that consumed + // nothing this time round. + for (vx_uint32 i = 0; i < moved; i++) { agoNotifyGraphParameterConsumed(graph, q->index); } } @@ -561,6 +607,7 @@ int agoExecuteGraphQueueManual(AgoGraph * graph) return VX_FAILURE; int overall_status = VX_SUCCESS; + vx_uint32 batches = 0; for (;;) { // Check if every enabled queue has at least one ready ref. bool all_ready = true; @@ -578,11 +625,20 @@ int agoExecuteGraphQueueManual(AgoGraph * graph) break; int status = agoExecutePipelinedGraphOnce(graph); + batches++; if (status != VX_SUCCESS) { overall_status = status; break; } } + // In this mode references for every graph parameter have to be enqueued + // before the graph is scheduled; if nothing could run, say so instead of + // reporting a successful schedule that did no work. + if (overall_status == VX_SUCCESS && batches == 0) { + agoAddLogEntry(&graph->ref, VX_FAILURE, + "ERROR: vxScheduleGraph: QUEUE_MANUAL requires a ready reference at every enqueued graph parameter\n"); + return VX_FAILURE; + } return overall_status; } @@ -731,4 +787,5 @@ void agoStartGraphPipeliningAutoExecutor(AgoGraph *) {} void agoStartGraphStreamingThread(AgoGraph *) {} vx_uint32 agoGetReferenceEnqueueCount(AgoContext *, AgoReference *) { return 0; } void agoRemoveEventRegistrations(AgoContext *, vx_reference) {} +vx_uint64 agoEventTimestampNs() { return 0; } #endif diff --git a/amd_openvx/openvx/api/vx_pipelining_api.cpp b/amd_openvx/openvx/api/vx_pipelining_api.cpp index bae0ab532..cff2fa9ae 100644 --- a/amd_openvx/openvx/api/vx_pipelining_api.cpp +++ b/amd_openvx/openvx/api/vx_pipelining_api.cpp @@ -72,17 +72,23 @@ VX_API_ENTRY vx_status VX_API_CALL vxSetGraphScheduleConfig( return VX_ERROR_INVALID_PARAMETERS; if (p.refs_list_size == 0) return VX_ERROR_INVALID_PARAMETERS; + // The reference list is what lets the queue be validated up front, + // so the spec requires the implementation to check it is present. + if (!p.refs_list) + return VX_ERROR_INVALID_PARAMETERS; + // The spec also says graph_parameter_index must be unique across the + // list, but that is a requirement on the application and it is not + // policed here: GraphPipeline.ScalarOutput configures index 1 twice + // (leaving index 2 unconfigured) and must still be accepted. pipe->param_queues[index].get()->max_depth = p.refs_list_size; pipe->param_queues[index].get()->enabled = true; - if (p.refs_list) { - for (vx_uint32 j = 0; j < p.refs_list_size; j++) { - vx_reference ref = p.refs_list[j]; - if (!ref) - return VX_ERROR_INVALID_PARAMETERS; - if (!agoIsValidReference((AgoReference *)ref)) - return VX_ERROR_INVALID_REFERENCE; - pipe->param_queues[index].get()->valid_refs.push_back((AgoData *)ref); - } + for (vx_uint32 j = 0; j < p.refs_list_size; j++) { + vx_reference ref = p.refs_list[j]; + if (!ref) + return VX_ERROR_INVALID_PARAMETERS; + if (!agoIsValidReference((AgoReference *)ref)) + return VX_ERROR_INVALID_REFERENCE; + pipe->param_queues[index].get()->valid_refs.push_back((AgoData *)ref); } } @@ -101,7 +107,7 @@ VX_API_ENTRY vx_status VX_API_CALL vxGetGraphParameterRefsList( vx_uint32 ref_list_size, vx_reference refs_list[]) { - vx_status status = VX_ERROR_INVALID_REFERENCE; + vx_status status = VX_ERROR_INVALID_GRAPH; if (agoIsValidGraph(graph) && graph->verified) { status = VX_ERROR_INVALID_PARAMETERS; AgoGraphPipeliningState * pipe = agoGetGraphPipeliningState(graph); @@ -124,7 +130,7 @@ VX_API_ENTRY vx_status VX_API_CALL vxAddReferencesToGraphParameterList( vx_uint32 number_to_add, const vx_reference new_references[]) { - vx_status status = VX_ERROR_INVALID_REFERENCE; + vx_status status = VX_ERROR_INVALID_GRAPH; if (agoIsValidGraph(graph) && graph->verified) { status = VX_ERROR_INVALID_PARAMETERS; if (number_to_add == 0 || !new_references) @@ -170,7 +176,11 @@ VX_API_ENTRY vx_status VX_API_CALL vxGraphParameterEnqueueReadyRef( if (!param || param->direction != VX_OUTPUT) return status; q->enabled = true; - q->max_depth = num_refs; + // No configured depth for a queue the schedule config never covered, + // so leave it unbounded rather than inferring one from this call. + // GraphPipeline.ScalarOutput relies on this path, because the CTS + // configures graph parameter 1 twice and never configures 2. + q->max_depth = 0; } for (vx_uint32 i = 0; i < num_refs; i++) { @@ -189,6 +199,12 @@ VX_API_ENTRY vx_status VX_API_CALL vxGraphParameterEnqueueReadyRef( return VX_ERROR_INVALID_PARAMETERS; } std::lock_guard lock(q->mtx); + // refs_list_size given at schedule-config time is the queue depth. + // Counting only the refs still waiting to be picked up keeps this a + // limit on what the application has handed over but the graph has + // not yet taken, which is what the depth is there to bound. + if (q->max_depth && q->ready_refs.size() >= (size_t)q->max_depth) + return VX_ERROR_NO_RESOURCES; q->ready_refs.push_back((AgoData *)refs[i]); } { @@ -277,7 +293,12 @@ VX_API_ENTRY vx_status VX_API_CALL vxEnableEvents(vx_context context) AgoContextEventSystem * evsys = agoGetContextEventSystem((AgoContext *)context); if (!evsys) return VX_FAILURE; - evsys->enabled = true; + { + std::lock_guard lock(evsys->events_mtx); + evsys->enabled = true; + } + // Wake anyone parked in a blocking vxWaitEvent while events were disabled. + evsys->events_cv.notify_all(); return VX_SUCCESS; } @@ -288,6 +309,9 @@ VX_API_ENTRY vx_status VX_API_CALL vxDisableEvents(vx_context context) AgoContextEventSystem * evsys = agoGetContextEventSystem((AgoContext *)context); if (!evsys) return VX_FAILURE; + // Events already queued stay queued: disabling stops new ones being + // recorded, it does not discard what the application has not yet collected. + std::lock_guard lock(evsys->events_mtx); evsys->enabled = false; return VX_SUCCESS; } @@ -301,15 +325,17 @@ VX_API_ENTRY vx_status VX_API_CALL vxWaitEvent(vx_context context, vx_event_t *e return VX_FAILURE; std::unique_lock lock(evsys->events_mtx); + // A blocking wait stays blocked while events are disabled; it may only + // return once they have been re-enabled. + auto ready = [&evsys]() { return evsys->enabled && !evsys->events.empty(); }; if (do_not_block == vx_true_e) { if (evsys->events.empty()) return VX_FAILURE; } else { if (evsys->timeout_ms == VX_TIMEOUT_WAIT_FOREVER) { - evsys->events_cv.wait(lock, [&evsys]() { return !evsys->events.empty(); }); + evsys->events_cv.wait(lock, ready); } else { - if (!evsys->events_cv.wait_for(lock, std::chrono::milliseconds(evsys->timeout_ms), - [&evsys]() { return !evsys->events.empty(); })) + if (!evsys->events_cv.wait_for(lock, std::chrono::milliseconds(evsys->timeout_ms), ready)) return VX_FAILURE; } } @@ -359,7 +385,7 @@ VX_API_ENTRY vx_status VX_API_CALL vxSendUserEvent(vx_context context, vx_uint32 AgoEvent evt; evt.event_type = VX_EVENT_USER; - evt.timestamp = 0; // could use steady_clock + evt.timestamp = agoEventTimestampNs(); evt.app_value = app_value; evt.graph = nullptr; evt.node = nullptr; @@ -375,22 +401,55 @@ VX_API_ENTRY vx_status VX_API_CALL vxRegisterEvent(vx_reference ref, enum vx_eve if (!ref || !agoIsValidReference((AgoReference *)ref)) return VX_ERROR_INVALID_REFERENCE; AgoReference * r = (AgoReference *)ref; + + // The event type has to make sense for the kind of reference given: a graph + // reports parameter-consumed and graph-completed, a node reports + // node-completed and node-error. Anything else is not supported. + AgoGraph * graph = nullptr; + if (r->type == VX_TYPE_GRAPH) { + if (type != VX_EVENT_GRAPH_PARAMETER_CONSUMED && type != VX_EVENT_GRAPH_COMPLETED) + return VX_ERROR_NOT_SUPPORTED; + graph = (AgoGraph *)r; + } + else if (r->type == VX_TYPE_NODE) { + if (type != VX_EVENT_NODE_COMPLETED && type != VX_EVENT_NODE_ERROR) + return VX_ERROR_NOT_SUPPORTED; + graph = (AgoGraph *)r->scope; + } + else { + return VX_ERROR_NOT_SUPPORTED; + } + + // Registration has to happen while the graph can still be set up for it. + if (graph && agoIsValidGraph(graph) && graph->verified) + return VX_ERROR_NOT_SUPPORTED; + + if (type == VX_EVENT_GRAPH_PARAMETER_CONSUMED) { + if (!graph || !agoIsValidGraph(graph) || param >= (vx_uint32)graph->parameters.size()) + return VX_ERROR_INVALID_PARAMETERS; + } + AgoContextEventSystem * evsys = agoGetContextEventSystem(r->context); if (!evsys) return VX_FAILURE; - if (type != VX_EVENT_GRAPH_PARAMETER_CONSUMED && - type != VX_EVENT_GRAPH_COMPLETED && - type != VX_EVENT_NODE_COMPLETED && - type != VX_EVENT_NODE_ERROR) { - return VX_ERROR_NOT_SUPPORTED; + + vx_uint32 index = (type == VX_EVENT_GRAPH_PARAMETER_CONSUMED) ? param : 0; + std::lock_guard lock(evsys->registrations_mtx); + // Registering the same thing twice is not an error; it updates the stored + // app_value. There is only ever one app_value per reference/type/parameter. + for (auto& existing : evsys->registrations) { + if (existing.ref == ref && existing.event_type == type && + existing.graph_parameter_index == index) { + existing.app_value = app_value; + return VX_SUCCESS; + } } AgoEventRegistration reg; reg.ref = ref; reg.event_type = type; reg.app_value = app_value; - reg.graph_parameter_index = (type == VX_EVENT_GRAPH_PARAMETER_CONSUMED) ? param : 0; - std::lock_guard lock(evsys->registrations_mtx); + reg.graph_parameter_index = index; evsys->registrations.push_back(reg); return VX_SUCCESS; } From c4b28c0d9bc814a3abe44b159e946cf5bb82fa7d Mon Sep 17 00:00:00 2001 From: KiritiGowda Date: Mon, 3 Aug 2026 12:00:24 -0700 Subject: [PATCH 13/19] fix(pipelining): honor 1.1 spec for null refs_list and wait-while-disabled Two validations added in #4 are stricter than vx_khr_pipelining 1.1 allows. vxSetGraphScheduleConfig rejected a null refs_list. The spec states that when the API is called before vxVerifyGraph the refs_list field can be NULL if the handles are not available to the application yet, and that only refs_list_size must always be specified. Requiring the list makes that legal sequence fail, so the per-reference validation is now conditional on the list being supplied. vxWaitEvent required events to be enabled before a blocking wait could return. The spec states that events generated before vxDisableEvents are still returned via vxWaitEvent, and that only additional events are withheld until events are enabled again. Gating the wait on the enabled flag stranded already-queued events and could block a caller indefinitely. Suppression of new events is already handled where they are recorded, so the wait predicate only needs to check whether an event is available. Neither path is covered by the conformance suite, which is why #4 passed at 5957/5957 with the stricter behavior. Co-authored-by: Cursor --- amd_openvx/openvx/api/vx_pipelining_api.cpp | 31 +++++++++++---------- 1 file changed, 17 insertions(+), 14 deletions(-) diff --git a/amd_openvx/openvx/api/vx_pipelining_api.cpp b/amd_openvx/openvx/api/vx_pipelining_api.cpp index cff2fa9ae..fe6a21e3a 100644 --- a/amd_openvx/openvx/api/vx_pipelining_api.cpp +++ b/amd_openvx/openvx/api/vx_pipelining_api.cpp @@ -72,23 +72,24 @@ VX_API_ENTRY vx_status VX_API_CALL vxSetGraphScheduleConfig( return VX_ERROR_INVALID_PARAMETERS; if (p.refs_list_size == 0) return VX_ERROR_INVALID_PARAMETERS; - // The reference list is what lets the queue be validated up front, - // so the spec requires the implementation to check it is present. - if (!p.refs_list) - return VX_ERROR_INVALID_PARAMETERS; + // vx_khr_pipelining 1.1: called before vxVerifyGraph, refs_list may + // be NULL when the application does not have the handles yet, while + // refs_list_size must always be given. So only the size is required. // The spec also says graph_parameter_index must be unique across the // list, but that is a requirement on the application and it is not // policed here: GraphPipeline.ScalarOutput configures index 1 twice // (leaving index 2 unconfigured) and must still be accepted. pipe->param_queues[index].get()->max_depth = p.refs_list_size; pipe->param_queues[index].get()->enabled = true; - for (vx_uint32 j = 0; j < p.refs_list_size; j++) { - vx_reference ref = p.refs_list[j]; - if (!ref) - return VX_ERROR_INVALID_PARAMETERS; - if (!agoIsValidReference((AgoReference *)ref)) - return VX_ERROR_INVALID_REFERENCE; - pipe->param_queues[index].get()->valid_refs.push_back((AgoData *)ref); + if (p.refs_list) { + for (vx_uint32 j = 0; j < p.refs_list_size; j++) { + vx_reference ref = p.refs_list[j]; + if (!ref) + return VX_ERROR_INVALID_PARAMETERS; + if (!agoIsValidReference((AgoReference *)ref)) + return VX_ERROR_INVALID_REFERENCE; + pipe->param_queues[index].get()->valid_refs.push_back((AgoData *)ref); + } } } @@ -325,9 +326,11 @@ VX_API_ENTRY vx_status VX_API_CALL vxWaitEvent(vx_context context, vx_event_t *e return VX_FAILURE; std::unique_lock lock(evsys->events_mtx); - // A blocking wait stays blocked while events are disabled; it may only - // return once they have been re-enabled. - auto ready = [&evsys]() { return evsys->enabled && !evsys->events.empty(); }; + // vx_khr_pipelining 1.1: events generated before vxDisableEvents are still + // returned here, so the wait must not require events to be enabled. What + // disabling does is stop new ones being recorded, which agoPushEvent already + // handles, so nothing further can arrive until they are re-enabled. + auto ready = [&evsys]() { return !evsys->events.empty(); }; if (do_not_block == vx_true_e) { if (evsys->events.empty()) return VX_FAILURE; From 528baf9172237ed4ed13536306ffc53ef71c6b9e Mon Sep 17 00:00:00 2001 From: KiritiGowda Date: Mon, 3 Aug 2026 12:56:11 -0700 Subject: [PATCH 14/19] feat(pipelining): accept refs_list from a vxSetGraphScheduleConfig after verify Allowing a null refs_list before verify is only useful if the application can hand the references over later, and the spec says how: "Application can call vxSetGraphScheduleConfig again after verify graph with all parameters remaining the same except with refs_list field providing the list of references that can be enqueued at the graph parameter." Every call was previously rejected once the graph was verified, so that second call returned VX_ERROR_INVALID_REFERENCE and the references could never be supplied. A post-verify call now records the reference lists without disturbing the queues, which were already set up before verification. The schedule mode has to match the mode configured earlier, since anything else would be a reconfiguration rather than supplying references. The remaining cross-call consistency the spec asks for is a requirement on the application and carries no error code, so it is not policed; that follows the same reasoning as the unique graph_parameter_index requirement, which the conformance suite itself does not honour. Co-authored-by: Cursor --- amd_openvx/openvx/api/vx_pipelining_api.cpp | 60 ++++++++++++++++++++- 1 file changed, 59 insertions(+), 1 deletion(-) diff --git a/amd_openvx/openvx/api/vx_pipelining_api.cpp b/amd_openvx/openvx/api/vx_pipelining_api.cpp index fe6a21e3a..28c342754 100644 --- a/amd_openvx/openvx/api/vx_pipelining_api.cpp +++ b/amd_openvx/openvx/api/vx_pipelining_api.cpp @@ -28,6 +28,54 @@ THE SOFTWARE. #if OPENVX_USE_PIPELINING +// Records the reference lists supplied by a post-verify vxSetGraphScheduleConfig +// call. The queues themselves were already set up before verify, so this must not +// disturb them beyond replacing the set of references allowed at each parameter. +static vx_status updateGraphScheduleRefsList( + vx_graph graph, + vx_enum graph_schedule_mode, + AgoGraphPipeliningState * pipe, + vx_uint32 graph_parameters_list_size, + const vx_graph_parameter_queue_params_t graph_parameters_queue_params_list[]) +{ + CAgoLock lock(graph->cs); + // Only the reference lists may differ from the call made before verify, so a + // different schedule mode is a reconfiguration and cannot be honoured now. + if (pipe->schedule_mode != graph_schedule_mode) + return VX_ERROR_INVALID_PARAMETERS; + if (pipe->param_queues.size() != graph->parameters.size()) + return VX_FAILURE; + for (vx_uint32 i = 0; i < graph_parameters_list_size; i++) { + const vx_graph_parameter_queue_params_t & p = graph_parameters_queue_params_list[i]; + vx_uint32 index = p.graph_parameter_index; + if (index >= (vx_uint32)graph->parameters.size()) + return VX_ERROR_INVALID_PARAMETERS; + if (p.refs_list_size == 0) + return VX_ERROR_INVALID_PARAMETERS; + AgoGraphParameterQueue * q = pipe->param_queues[index].get(); + // Queuing has to have been requested for this parameter before verify; + // this call may only fill in references, not enable a new queue. + if (!q || !q->enabled) + return VX_ERROR_INVALID_PARAMETERS; + if (!p.refs_list) + continue; + std::vector refs; + refs.reserve(p.refs_list_size); + for (vx_uint32 j = 0; j < p.refs_list_size; j++) { + vx_reference ref = p.refs_list[j]; + if (!ref) + return VX_ERROR_INVALID_PARAMETERS; + if (!agoIsValidReference((AgoReference *)ref)) + return VX_ERROR_INVALID_REFERENCE; + refs.push_back((AgoData *)ref); + } + // Built separately so a bad entry late in the list leaves the queue's + // existing references untouched. + q->valid_refs = std::move(refs); + } + return VX_SUCCESS; +} + VX_API_ENTRY vx_status VX_API_CALL vxSetGraphScheduleConfig( vx_graph graph, vx_enum graph_schedule_mode, @@ -35,7 +83,7 @@ VX_API_ENTRY vx_status VX_API_CALL vxSetGraphScheduleConfig( const vx_graph_parameter_queue_params_t graph_parameters_queue_params_list[]) { vx_status status = VX_ERROR_INVALID_REFERENCE; - if (agoIsValidGraph(graph) && !graph->verified) { + if (agoIsValidGraph(graph)) { if ((graph_schedule_mode != VX_GRAPH_SCHEDULE_MODE_NORMAL) && (graph_schedule_mode != VX_GRAPH_SCHEDULE_MODE_QUEUE_AUTO) && (graph_schedule_mode != VX_GRAPH_SCHEDULE_MODE_QUEUE_MANUAL)) { @@ -55,6 +103,16 @@ VX_API_ENTRY vx_status VX_API_CALL vxSetGraphScheduleConfig( if (!pipe) return VX_FAILURE; + if (graph->verified) { + // vx_khr_pipelining 1.1: the application may call this again after + // verify with everything unchanged except refs_list, which is how it + // hands over the references when they were not available earlier. + // Nothing may be reconfigured now, so this only records the lists. + return updateGraphScheduleRefsList(graph, graph_schedule_mode, pipe, + graph_parameters_list_size, + graph_parameters_queue_params_list); + } + // Stop any active executor before reconfiguring. This has to happen outside // graph->cs because the executor runs the graph inside that section. agoStopGraphPipelining(graph); From ed2c9d7170e982c196de96c0573f644a5bfa3369 Mon Sep 17 00:00:00 2001 From: KiritiGowda Date: Mon, 3 Aug 2026 13:02:24 -0700 Subject: [PATCH 15/19] fix(pipelining): report graph completion for every execution The spec says of VX_EVENT_GRAPH_COMPLETED: "This event is generated every time a graph execution completes. Graph completion event is generated for both successful execution of a graph or abandoned execution of a graph." Two things were narrower than that. The event was only emitted from the pipelined execution path, so an application that registered for it and then ran the graph in the default schedule mode never heard anything. And it was gated on a successful status, so an abandoned execution stayed silent, which is the case where an application waiting on events most needs to be told. Completion is now reported for a plain execution as well, and is no longer conditional on the status. Nothing is reported where no execution was attempted, such as an unverified graph. Events still only reach applications that registered for the reference, so this is silent for everyone else. Co-authored-by: Cursor --- amd_openvx/openvx/ago/ago_interface.cpp | 12 ++++++++++++ amd_openvx/openvx/ago/ago_pipelining.cpp | 7 +++---- 2 files changed, 15 insertions(+), 4 deletions(-) diff --git a/amd_openvx/openvx/ago/ago_interface.cpp b/amd_openvx/openvx/ago/ago_interface.cpp index 1bae77cec..0b5c24888 100644 --- a/amd_openvx/openvx/ago/ago_interface.cpp +++ b/amd_openvx/openvx/ago/ago_interface.cpp @@ -2936,6 +2936,9 @@ int agoProcessGraph(AgoGraph * graph) } // execute graph if possible if (status == VX_SUCCESS) { + // The pipelined path reports completion for each of the executions it + // runs, so only a plain execution still has to be reported here. + bool reportCompletion = false; if (graph->verified && graph->pipelining) { AgoGraphPipeliningState * pipe = graph->pipelining; if (pipe->schedule_mode == VX_GRAPH_SCHEDULE_MODE_QUEUE_MANUAL || @@ -2945,6 +2948,7 @@ int agoProcessGraph(AgoGraph * graph) } else if (graph->isReadyToExecute) { status = agoExecuteGraph(graph); + reportCompletion = true; } else { agoAddLogEntry(&graph->ref, VX_FAILURE, "ERROR: agoProcessGraph: not verified (%d) or not ready to execute (%d)\n", graph->verified, graph->isReadyToExecute); @@ -2955,11 +2959,19 @@ int agoProcessGraph(AgoGraph * graph) // For non-streaming execution, pre-fill source-node pipeup queues. agoPreFillSourceNodePipeup(graph); status = agoExecuteGraph(graph); + reportCompletion = true; } else { agoAddLogEntry(&graph->ref, VX_FAILURE, "ERROR: agoProcessGraph: not verified (%d) or not ready to execute (%d)\n", graph->verified, graph->isReadyToExecute); status = VX_FAILURE; } + // vx_khr_pipelining 1.1: a graph completion event is generated every + // time a graph execution completes, for an abandoned execution as well + // as a successful one. Nothing is reported where no execution was + // attempted. Events still only reach applications that registered for + // them, so this is silent for everyone else. + if (reportCompletion) + agoNotifyGraphCompleted(graph); } } return status; diff --git a/amd_openvx/openvx/ago/ago_pipelining.cpp b/amd_openvx/openvx/ago/ago_pipelining.cpp index e55e5eff9..ec6be320a 100644 --- a/amd_openvx/openvx/ago/ago_pipelining.cpp +++ b/amd_openvx/openvx/ago/ago_pipelining.cpp @@ -588,10 +588,9 @@ int agoExecutePipelinedGraphOnce(AgoGraph * graph) agoEmitRegisteredNodeEvents(graph, VX_EVENT_NODE_COMPLETED, VX_SUCCESS); } - // Emit graph completion event. - if (status == VX_SUCCESS) { - agoNotifyGraphCompleted(graph); - } + // vx_khr_pipelining 1.1: graph completion is reported for an abandoned + // execution as well as a successful one, so this is not gated on status. + agoNotifyGraphCompleted(graph); return status; } From eec6d9cde6be0af3191752a33af1a0ac97704094 Mon Sep 17 00:00:00 2001 From: KiritiGowda Date: Mon, 3 Aug 2026 13:04:07 -0700 Subject: [PATCH 16/19] test(pipelining): add an API test for the extension's specified behaviour The conformance suite drives the pipelining extension through a handful of end-to-end scenarios, which leaves much of the specification untested: argument validation, the error codes the spec names, the event registration rules, and what happens to events around vxEnableEvents/vxDisableEvents. Every fix in this branch was found by reading the spec rather than by a failing test, which is a sign that the gap is worth closing. This adds tests/openvx_api_tests/pipelining_api, following the existing API test layout: a standalone CMake project run from CTest and from both conformance workflows, on the CPU and GPU AGO targets, against the instrumented library so the paths count towards coverage. Assertions tied to specific wording carry a [spec] comment naming the requirement, and behaviour the spec leaves implementation-defined is recorded as this implementation's choice rather than as a requirement. Covered: schedule configuration validation including a null refs_list before verify and a refs_list supplied after verify; queue enqueue/dequeue validation and the depth limit; event registration type and ordering rules and the replacement of app_value on re-registration; delivery restricted to registered references; events queued before a disable still being returned while new ones are withheld; user events; the timeout and pipeline depth attributes; QUEUE_MANUAL and QUEUE_AUTO end to end with a check that the queued references really carried the data; and streaming enable/start/stop. Every blocking wait is bounded by a timeout attribute so a lost wake-up fails the test instead of hanging the job, and the whole suite skips cleanly when the extension is compiled out. The pipelining and streaming jobs were also missing from the coverage job's dependencies in both workflows, so the profile data from exactly the code under discussion could be raced or dropped rather than merged into the report. Co-authored-by: Cursor --- .github/workflows/conformance-hip.yml | 5 +- .github/workflows/conformance.yml | 5 +- tests/CMakeLists.txt | 13 + .../pipelining_api/CMakeLists.txt | 52 + .../pipelining_api/pipelining_api.cpp | 1014 +++++++++++++++++ 5 files changed, 1087 insertions(+), 2 deletions(-) create mode 100644 tests/openvx_api_tests/pipelining_api/CMakeLists.txt create mode 100644 tests/openvx_api_tests/pipelining_api/pipelining_api.cpp diff --git a/.github/workflows/conformance-hip.yml b/.github/workflows/conformance-hip.yml index cdef00320..bb0fcaa69 100644 --- a/.github/workflows/conformance-hip.yml +++ b/.github/workflows/conformance-hip.yml @@ -237,7 +237,7 @@ jobs: canny channel_extract color_convert accumulate graph graph_api tensor_api data_objects user_kernel tensor_advanced threshold_query graph_import vxu_api - coverage_boost vision_coverage + coverage_boost vision_coverage pipelining_api ) # libopenvx.so (HIP backend) has unresolved HIP runtime symbols # (e.g. hipSetDevice@hip_4.2) that must be satisfied by @@ -1198,6 +1198,7 @@ jobs: "vxu_api:openvx_vxu_api" "coverage_boost:openvx_coverage_boost" "vision_coverage:openvx_vision_coverage" + "pipelining_api:openvx_pipelining_api" ) # Run the API test set on both AGO targets. In the HIP build the CPU # HAF kernels and the HIP kernels are both compiled/instrumented, so @@ -1641,6 +1642,8 @@ jobs: - vision-features-hip - vision-statistics-hip - vision-pyramid-hip + - pipelining-hip + - streaming-hip - api-tests-hip - gdf-tests-hip - vision-tests-hip diff --git a/.github/workflows/conformance.yml b/.github/workflows/conformance.yml index 57770f309..415dedd61 100644 --- a/.github/workflows/conformance.yml +++ b/.github/workflows/conformance.yml @@ -134,7 +134,7 @@ jobs: canny channel_extract color_convert accumulate graph graph_api tensor_api data_objects user_kernel tensor_advanced threshold_query graph_import vxu_api - coverage_boost vision_coverage + coverage_boost vision_coverage pipelining_api ) for test_dir in "${API_TESTS[@]}"; do echo "::group::Building ${test_dir}" @@ -731,6 +731,7 @@ jobs: "vxu_api:openvx_vxu_api" "coverage_boost:openvx_coverage_boost" "vision_coverage:openvx_vision_coverage" + "pipelining_api:openvx_pipelining_api" ) for entry in "${API_EXECUTABLES[@]}"; do dir="${entry%%:*}" @@ -1132,6 +1133,8 @@ jobs: - vision-features - vision-statistics - vision-pyramid + - pipelining-cpu + - streaming-cpu - api-tests - gdf-tests - vision-tests diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index f97742765..4a3d07c7f 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -291,6 +291,19 @@ add_test( ) set_property(TEST openvx_vision_coverage PROPERTY ENVIRONMENT "AGO_DEFAULT_TARGET=CPU") +# 11_l - pipelining_api - graph pipelining/streaming/event extension API test +add_test( + NAME + openvx_pipelining_api + COMMAND + "${CMAKE_CTEST_COMMAND}" + --build-and-test "${CMAKE_CURRENT_SOURCE_DIR}/openvx_api_tests/pipelining_api" + "${CMAKE_CURRENT_BINARY_DIR}/pipelining_api" + --build-generator "${CMAKE_GENERATOR}" + --test-command "openvx_pipelining_api" +) +set_property(TEST openvx_pipelining_api PROPERTY ENVIRONMENT "AGO_DEFAULT_TARGET=CPU") + # 12 - canny - vision graph force to CPU add_test(NAME openvx_canny_CPU COMMAND openvx_canny diff --git a/tests/openvx_api_tests/pipelining_api/CMakeLists.txt b/tests/openvx_api_tests/pipelining_api/CMakeLists.txt new file mode 100644 index 000000000..babcce9b6 --- /dev/null +++ b/tests/openvx_api_tests/pipelining_api/CMakeLists.txt @@ -0,0 +1,52 @@ +################################################################################ +# +# MIT License +# +# Copyright (c) 2026 Advanced Micro Devices, Inc. +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in all +# copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +# SOFTWARE. +# +################################################################################ + +cmake_minimum_required(VERSION 3.10) + +# ROCM Path +if(DEFINED ENV{ROCM_PATH}) + set(ROCM_PATH $ENV{ROCM_PATH} CACHE PATH "Default ROCm installation path") +elseif(ROCM_PATH) + message("-- INFO:ROCM_PATH Set -- ${ROCM_PATH}") +else() + set(ROCM_PATH /opt/rocm CACHE PATH "Default ROCm installation path") +endif() +# Set AMD Clang as default compiler +set(CMAKE_CXX_STANDARD 17) +set(CMAKE_CXX_STANDARD_REQUIRED On) +set(CMAKE_CXX_EXTENSIONS ON) +if(NOT DEFINED CMAKE_CXX_COMPILER AND EXISTS "${ROCM_PATH}/lib/llvm/bin/amdclang++") + set(CMAKE_C_COMPILER ${ROCM_PATH}/lib/llvm/bin/amdclang) + set(CMAKE_CXX_COMPILER ${ROCM_PATH}/lib/llvm/bin/amdclang++) +endif() + +project (openvx_pipelining_api) + +include_directories (${ROCM_PATH}/include/mivisionx) +link_directories (${ROCM_PATH}/lib) + +add_executable(openvx_pipelining_api pipelining_api.cpp) +target_link_libraries(${PROJECT_NAME} openvx) diff --git a/tests/openvx_api_tests/pipelining_api/pipelining_api.cpp b/tests/openvx_api_tests/pipelining_api/pipelining_api.cpp new file mode 100644 index 000000000..b48c58117 --- /dev/null +++ b/tests/openvx_api_tests/pipelining_api/pipelining_api.cpp @@ -0,0 +1,1014 @@ +/* +Copyright (c) 2026 Advanced Micro Devices, Inc. All rights reserved. + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. +*/ + +// Graph Pipelining extension (vx_khr_pipelining) API coverage test. +// +// The Khronos conformance suite exercises the pipelining extension through a +// handful of end-to-end scenarios, which leaves a good deal of the specified +// behaviour untested: argument validation, the error codes the spec names, the +// event registration rules, and what happens to events around +// vxEnableEvents/vxDisableEvents. Those are the parts covered here, alongside +// one end-to-end queueing case that checks the data actually flows through the +// queued references rather than the defaults bound at verify time. +// +// Behaviours asserted against specific wording in the 1.1 specification are +// marked with a [spec] comment naming the requirement. + +#include +#include +#include +#include +#include +#include + +static const vx_uint32 IMG_W = 64; +static const vx_uint32 IMG_H = 64; + +// Bounds every blocking wait in this test so a regression that fails to wake a +// waiter shows up as a test failure rather than a hung CI job. +static const vx_uint32 WAIT_TIMEOUT_MS = 10000; + +#define CHECK_STATUS(call) do { \ + vx_status s_ = (call); \ + if (s_ != VX_SUCCESS) { \ + printf(" FAIL: %s returned %d at %s:%d\n", #call, s_, __FILE__, __LINE__); \ + errors++; \ + } \ +} while(0) + +#define CHECK_NOT_NULL(obj, name) do { \ + if (!(obj)) { \ + printf(" FAIL: %s is NULL at %s:%d\n", name, __FILE__, __LINE__); \ + errors++; \ + } \ +} while(0) + +// Asserts an exact status, which is the point of most of these cases: the spec +// names the error code, so returning a different failure is still wrong. +#define EXPECT_STATUS(call, expected, what) do { \ + vx_status s_ = (call); \ + if (s_ != (expected)) { \ + printf(" FAIL: %s -> %d, expected %d (%s) at %s:%d\n", \ + #call, s_, (int)(expected), what, __FILE__, __LINE__); \ + errors++; \ + } else { \ + printf(" PASS: %s\n", what); \ + } \ +} while(0) + +#define EXPECT_TRUE(cond, what) do { \ + if (!(cond)) { \ + printf(" FAIL: %s at %s:%d\n", what, __FILE__, __LINE__); \ + errors++; \ + } else { \ + printf(" PASS: %s\n", what); \ + } \ +} while(0) + +// --------------------------------------------------------------------------- +// helpers +// --------------------------------------------------------------------------- + +static vx_status fill_u8_image(vx_image img, vx_uint8 value) +{ + std::vector buf((size_t)IMG_W * IMG_H, value); + vx_rectangle_t rect = { 0, 0, IMG_W, IMG_H }; + vx_imagepatch_addressing_t addr; + memset(&addr, 0, sizeof(addr)); + addr.dim_x = IMG_W; + addr.dim_y = IMG_H; + addr.stride_x = 1; + addr.stride_y = (vx_int32)IMG_W; + return vxCopyImagePatch(img, &rect, 0, &addr, buf.data(), VX_WRITE_ONLY, VX_MEMORY_TYPE_HOST); +} + +// Reads the image back and reports whether every pixel holds the expected value. +static bool u8_image_is_uniform(vx_image img, vx_uint8 expected) +{ + std::vector buf((size_t)IMG_W * IMG_H, 0); + vx_rectangle_t rect = { 0, 0, IMG_W, IMG_H }; + vx_imagepatch_addressing_t addr; + memset(&addr, 0, sizeof(addr)); + addr.dim_x = IMG_W; + addr.dim_y = IMG_H; + addr.stride_x = 1; + addr.stride_y = (vx_int32)IMG_W; + if (vxCopyImagePatch(img, &rect, 0, &addr, buf.data(), VX_READ_ONLY, VX_MEMORY_TYPE_HOST) != VX_SUCCESS) + return false; + for (size_t i = 0; i < buf.size(); i++) { + if (buf[i] != expected) + return false; + } + return true; +} + +// Collects every event currently queued, so a test can assert on exactly what +// the implementation reported for one execution. +struct EventTally { + vx_uint32 total = 0; + vx_uint32 graph_completed = 0; + vx_uint32 node_completed = 0; + vx_uint32 node_error = 0; + vx_uint32 parameter_consumed = 0; + vx_uint32 user = 0; + std::vector app_values; +}; + +static EventTally drain_events(vx_context context) +{ + EventTally t; + vx_event_t ev; + while (true) { + memset(&ev, 0, sizeof(ev)); + if (vxWaitEvent(context, &ev, vx_true_e) != VX_SUCCESS) + break; + t.total++; + t.app_values.push_back(ev.app_value); + switch (ev.type) { + case VX_EVENT_GRAPH_COMPLETED: t.graph_completed++; break; + case VX_EVENT_NODE_COMPLETED: t.node_completed++; break; + case VX_EVENT_NODE_ERROR: t.node_error++; break; + case VX_EVENT_GRAPH_PARAMETER_CONSUMED: t.parameter_consumed++; break; + case VX_EVENT_USER: t.user++; break; + default: break; + } + } + return t; +} + +// A graph holding one NOT node, with the input as graph parameter 0 and the +// output as graph parameter 1. +struct NotGraph { + vx_graph graph; + vx_node node; + vx_image in; + vx_image out; +}; + +static NotGraph make_not_graph(vx_context context, vx_uint32 num_params) +{ + NotGraph g; + g.graph = vxCreateGraph(context); + g.in = vxCreateImage(context, IMG_W, IMG_H, VX_DF_IMAGE_U8); + g.out = vxCreateImage(context, IMG_W, IMG_H, VX_DF_IMAGE_U8); + g.node = vxNotNode(g.graph, g.in, g.out); + for (vx_uint32 i = 0; i < num_params; i++) { + vx_parameter p = vxGetParameterByIndex(g.node, i); + if (p) { + vxAddParameterToGraph(g.graph, p); + vxReleaseParameter(&p); + } + } + return g; +} + +static void release_not_graph(NotGraph & g) +{ + if (g.node) vxReleaseNode(&g.node); + if (g.in) vxReleaseImage(&g.in); + if (g.out) vxReleaseImage(&g.out); + if (g.graph) vxReleaseGraph(&g.graph); +} + +static void set_event_timeout(vx_context context) +{ + vx_uint32 timeout = WAIT_TIMEOUT_MS; + vxSetContextAttribute(context, VX_CONTEXT_EVENT_TIMEOUT, &timeout, sizeof(timeout)); +} + +// --------------------------------------------------------------------------- +// Test 1: vxSetGraphScheduleConfig argument validation +// --------------------------------------------------------------------------- +static int test_schedule_config_validation() +{ + int errors = 0; + printf("\n=== Test 1: vxSetGraphScheduleConfig validation ===\n"); + + vx_context context = vxCreateContext(); + NotGraph g = make_not_graph(context, 2); + CHECK_NOT_NULL(g.node, "vxNotNode"); + + vx_reference in_ref = (vx_reference)g.in; + vx_graph_parameter_queue_params_t q[2]; + memset(q, 0, sizeof(q)); + q[0].graph_parameter_index = 0; + q[0].refs_list_size = 1; + q[0].refs_list = &in_ref; + + EXPECT_STATUS(vxSetGraphScheduleConfig(nullptr, VX_GRAPH_SCHEDULE_MODE_QUEUE_MANUAL, 1, q), + VX_ERROR_INVALID_REFERENCE, "null graph rejected"); + + EXPECT_STATUS(vxSetGraphScheduleConfig(g.graph, (vx_enum)0x7FFFFFFF, 1, q), + VX_ERROR_INVALID_PARAMETERS, "unknown schedule mode rejected"); + + // NORMAL mode takes no queue configuration at all. + EXPECT_STATUS(vxSetGraphScheduleConfig(g.graph, VX_GRAPH_SCHEDULE_MODE_NORMAL, 0, nullptr), + VX_SUCCESS, "NORMAL mode with no queue list accepted"); + EXPECT_STATUS(vxSetGraphScheduleConfig(g.graph, VX_GRAPH_SCHEDULE_MODE_NORMAL, 1, q), + VX_ERROR_INVALID_PARAMETERS, "NORMAL mode with a queue list rejected"); + + // The queueing modes require one. + EXPECT_STATUS(vxSetGraphScheduleConfig(g.graph, VX_GRAPH_SCHEDULE_MODE_QUEUE_MANUAL, 0, q), + VX_ERROR_INVALID_PARAMETERS, "QUEUE_MANUAL with zero list size rejected"); + EXPECT_STATUS(vxSetGraphScheduleConfig(g.graph, VX_GRAPH_SCHEDULE_MODE_QUEUE_MANUAL, 1, nullptr), + VX_ERROR_INVALID_PARAMETERS, "QUEUE_MANUAL with null list rejected"); + + // [spec] refs_list_size MUST always be specified by the application. + q[0].refs_list_size = 0; + EXPECT_STATUS(vxSetGraphScheduleConfig(g.graph, VX_GRAPH_SCHEDULE_MODE_QUEUE_MANUAL, 1, q), + VX_ERROR_INVALID_PARAMETERS, "refs_list_size of zero rejected"); + q[0].refs_list_size = 1; + + // Out of range graph parameter index. + q[0].graph_parameter_index = 99; + EXPECT_STATUS(vxSetGraphScheduleConfig(g.graph, VX_GRAPH_SCHEDULE_MODE_QUEUE_MANUAL, 1, q), + VX_ERROR_INVALID_PARAMETERS, "out of range graph parameter index rejected"); + q[0].graph_parameter_index = 0; + + // A null entry inside a supplied refs_list is not a usable reference. + vx_reference bad_list[2] = { (vx_reference)g.in, nullptr }; + q[0].refs_list_size = 2; + q[0].refs_list = bad_list; + EXPECT_STATUS(vxSetGraphScheduleConfig(g.graph, VX_GRAPH_SCHEDULE_MODE_QUEUE_MANUAL, 1, q), + VX_ERROR_INVALID_PARAMETERS, "null entry in refs_list rejected"); + + // [spec] "When this API is called before vxVerifyGraph, the refs_list field + // can be NULL, if the reference handles are not available yet at the + // application. However refs_list_size MUST always be specified." + q[0].refs_list_size = 2; + q[0].refs_list = nullptr; + EXPECT_STATUS(vxSetGraphScheduleConfig(g.graph, VX_GRAPH_SCHEDULE_MODE_QUEUE_MANUAL, 1, q), + VX_SUCCESS, "null refs_list accepted before verify"); + + // The configured mode is observable through the graph attribute. + { + vx_enum mode = 0; + CHECK_STATUS(vxQueryGraph(g.graph, VX_GRAPH_SCHEDULE_MODE, &mode, sizeof(mode))); + EXPECT_TRUE(mode == VX_GRAPH_SCHEDULE_MODE_QUEUE_MANUAL, + "VX_GRAPH_SCHEDULE_MODE reports the configured mode"); + } + + // [spec] both of these report VX_ERROR_INVALID_GRAPH when the graph is not + // verified; this graph never is. Adding references in particular "may only + // be called after graph verification". + { + vx_reference got[2] = { nullptr, nullptr }; + EXPECT_STATUS(vxGetGraphParameterRefsList(g.graph, 0, 2, got), + VX_ERROR_INVALID_GRAPH, "refs list query before verify rejected"); + vx_reference add = (vx_reference)g.in; + EXPECT_STATUS(vxAddReferencesToGraphParameterList(g.graph, 0, 1, &add), + VX_ERROR_INVALID_GRAPH, "adding references before verify rejected"); + } + + release_not_graph(g); + vxReleaseContext(&context); + return errors; +} + +// --------------------------------------------------------------------------- +// Test 2: refs_list handed over by a second call made after vxVerifyGraph +// --------------------------------------------------------------------------- +static int test_refs_list_after_verify() +{ + int errors = 0; + printf("\n=== Test 2: refs_list supplied after vxVerifyGraph ===\n"); + + vx_context context = vxCreateContext(); + NotGraph g = make_not_graph(context, 2); + vx_image spare = vxCreateImage(context, IMG_W, IMG_H, VX_DF_IMAGE_U8); + vx_image unlisted = vxCreateImage(context, IMG_W, IMG_H, VX_DF_IMAGE_U8); + + vx_graph_parameter_queue_params_t q[1]; + memset(q, 0, sizeof(q)); + q[0].graph_parameter_index = 0; + q[0].refs_list_size = 2; + q[0].refs_list = nullptr; + + EXPECT_STATUS(vxSetGraphScheduleConfig(g.graph, VX_GRAPH_SCHEDULE_MODE_QUEUE_MANUAL, 1, q), + VX_SUCCESS, "configured before verify without the handles"); + EXPECT_STATUS(vxVerifyGraph(g.graph), VX_SUCCESS, "vxVerifyGraph"); + + // [spec] "Application can call vxSetGraphScheduleConfig again after verify + // graph with all parameters remaining the same except with refs_list field + // providing the list of references that can be enqueued." + vx_reference refs[2] = { (vx_reference)g.in, (vx_reference)spare }; + q[0].refs_list = refs; + EXPECT_STATUS(vxSetGraphScheduleConfig(g.graph, VX_GRAPH_SCHEDULE_MODE_QUEUE_MANUAL, 1, q), + VX_SUCCESS, "refs_list accepted after verify"); + + // The list is now readable back through the query API. + { + vx_reference got[2] = { nullptr, nullptr }; + EXPECT_STATUS(vxGetGraphParameterRefsList(g.graph, 0, 2, got), + VX_SUCCESS, "vxGetGraphParameterRefsList"); + EXPECT_TRUE(got[0] == (vx_reference)g.in && got[1] == (vx_reference)spare, + "returned refs_list matches what was supplied"); + // [spec] a null refs_list, or a size too small to hold the list, is an + // invalid request once the graph is verified. + EXPECT_STATUS(vxGetGraphParameterRefsList(g.graph, 0, 2, nullptr), + VX_ERROR_INVALID_PARAMETERS, "null refs_list output rejected"); + EXPECT_STATUS(vxGetGraphParameterRefsList(g.graph, 0, 1, got), + VX_ERROR_INVALID_PARAMETERS, "undersized refs_list output rejected"); + EXPECT_STATUS(vxGetGraphParameterRefsList(g.graph, 99, 2, got), + VX_ERROR_INVALID_PARAMETERS, "out of range parameter rejected"); + } + + // A reference from the list can be enqueued. Enqueueing one outside the list + // is left implementation-defined by the spec; this implementation rejects it, + // which is what the list is checked against here. + { + vx_reference enq = (vx_reference)g.in; + EXPECT_STATUS(vxGraphParameterEnqueueReadyRef(g.graph, 0, &enq, 1), + VX_SUCCESS, "listed reference can be enqueued"); + vx_reference bad = (vx_reference)unlisted; + EXPECT_STATUS(vxGraphParameterEnqueueReadyRef(g.graph, 0, &bad, 1), + VX_ERROR_INVALID_PARAMETERS, "unlisted reference is rejected"); + } + + // vxAddReferencesToGraphParameterList extends the permitted set. + { + vx_reference add = (vx_reference)unlisted; + EXPECT_STATUS(vxAddReferencesToGraphParameterList(g.graph, 0, 1, &add), + VX_SUCCESS, "vxAddReferencesToGraphParameterList"); + EXPECT_STATUS(vxGraphParameterEnqueueReadyRef(g.graph, 0, &add, 1), + VX_SUCCESS, "newly added reference can be enqueued"); + } + + // [spec] the depth given as refs_list_size bounds what may be handed over + // and not yet taken by the graph; beyond it there is no resource left. + { + vx_reference enq = (vx_reference)spare; + EXPECT_STATUS(vxGraphParameterEnqueueReadyRef(g.graph, 0, &enq, 1), + VX_ERROR_NO_RESOURCES, "enqueue beyond the configured depth reports NO_RESOURCES"); + } + + // VX_REFERENCE_ENQUEUE_COUNT tracks how often a reference was enqueued. + { + vx_uint32 count = 0; + vx_status s = vxQueryReference((vx_reference)g.in, VX_REFERENCE_ENQUEUE_COUNT, + &count, sizeof(count)); + if (s == VX_SUCCESS) + printf(" PASS: VX_REFERENCE_ENQUEUE_COUNT = %u\n", count); + else + printf(" INFO: VX_REFERENCE_ENQUEUE_COUNT returned %d\n", s); + } + + vxReleaseImage(&spare); + vxReleaseImage(&unlisted); + release_not_graph(g); + vxReleaseContext(&context); + return errors; +} + +// --------------------------------------------------------------------------- +// Test 3: queue argument validation and vxGraphParameterCheckDoneRef +// --------------------------------------------------------------------------- +static int test_queue_validation() +{ + int errors = 0; + printf("\n=== Test 3: queue API validation ===\n"); + + vx_context context = vxCreateContext(); + NotGraph g = make_not_graph(context, 2); + + vx_reference in_ref = (vx_reference)g.in; + vx_reference out_ref = (vx_reference)g.out; + vx_graph_parameter_queue_params_t q[2]; + memset(q, 0, sizeof(q)); + q[0].graph_parameter_index = 0; + q[0].refs_list_size = 1; + q[0].refs_list = &in_ref; + q[1].graph_parameter_index = 1; + q[1].refs_list_size = 1; + q[1].refs_list = &out_ref; + CHECK_STATUS(vxSetGraphScheduleConfig(g.graph, VX_GRAPH_SCHEDULE_MODE_QUEUE_MANUAL, 2, q)); + CHECK_STATUS(vxVerifyGraph(g.graph)); + + EXPECT_STATUS(vxGraphParameterEnqueueReadyRef(nullptr, 0, &in_ref, 1), + VX_ERROR_INVALID_REFERENCE, "enqueue on a null graph rejected"); + EXPECT_STATUS(vxGraphParameterEnqueueReadyRef(g.graph, 99, &in_ref, 1), + VX_ERROR_INVALID_PARAMETERS, "enqueue on an out of range parameter rejected"); + EXPECT_STATUS(vxGraphParameterEnqueueReadyRef(g.graph, 0, nullptr, 1), + VX_ERROR_INVALID_PARAMETERS, "enqueue with a null reference array rejected"); + + { + vx_reference null_ref = nullptr; + EXPECT_STATUS(vxGraphParameterEnqueueReadyRef(g.graph, 0, &null_ref, 1), + VX_ERROR_INVALID_REFERENCE, "enqueue of a null reference rejected"); + } + + // Nothing has been consumed, so nothing is waiting to be dequeued. + { + vx_uint32 num = 99; + EXPECT_STATUS(vxGraphParameterCheckDoneRef(g.graph, 1, &num), + VX_SUCCESS, "vxGraphParameterCheckDoneRef"); + EXPECT_TRUE(num == 0, "no done references before any execution"); + EXPECT_STATUS(vxGraphParameterCheckDoneRef(g.graph, 1, nullptr), + VX_ERROR_INVALID_PARAMETERS, "check with a null count rejected"); + EXPECT_STATUS(vxGraphParameterCheckDoneRef(g.graph, 99, &num), + VX_ERROR_INVALID_PARAMETERS, "check on an out of range parameter rejected"); + } + + { + vx_reference deq = nullptr; + vx_uint32 num = 0; + EXPECT_STATUS(vxGraphParameterDequeueDoneRef(g.graph, 99, &deq, 1, &num), + VX_ERROR_INVALID_PARAMETERS, "dequeue on an out of range parameter rejected"); + EXPECT_STATUS(vxGraphParameterDequeueDoneRef(g.graph, 1, nullptr, 1, &num), + VX_ERROR_INVALID_PARAMETERS, "dequeue with a null reference array rejected"); + } + + // In QUEUE_MANUAL a graph execution consumes one reference from every + // configured queue, so with the queues empty there is nothing to run and the + // attempt cannot report success. vxProcessGraph is used rather than + // vxScheduleGraph because it carries the execution status back directly. + EXPECT_TRUE(vxProcessGraph(g.graph) != VX_SUCCESS, + "executing with nothing enqueued does not report success"); + + release_not_graph(g); + vxReleaseContext(&context); + return errors; +} + +// --------------------------------------------------------------------------- +// Test 4: vxRegisterEvent validation +// --------------------------------------------------------------------------- +static int test_event_registration_validation() +{ + int errors = 0; + printf("\n=== Test 4: vxRegisterEvent validation ===\n"); + + vx_context context = vxCreateContext(); + NotGraph g = make_not_graph(context, 2); + + EXPECT_STATUS(vxRegisterEvent(nullptr, VX_EVENT_GRAPH_COMPLETED, 0, 1), + VX_ERROR_INVALID_REFERENCE, "null reference rejected"); + + // [spec] VX_ERROR_NOT_SUPPORTED - type is not valid for the provided reference. + EXPECT_STATUS(vxRegisterEvent((vx_reference)g.graph, VX_EVENT_NODE_COMPLETED, 0, 1), + VX_ERROR_NOT_SUPPORTED, "NODE_COMPLETED on a graph rejected"); + EXPECT_STATUS(vxRegisterEvent((vx_reference)g.graph, VX_EVENT_NODE_ERROR, 0, 1), + VX_ERROR_NOT_SUPPORTED, "NODE_ERROR on a graph rejected"); + EXPECT_STATUS(vxRegisterEvent((vx_reference)g.node, VX_EVENT_GRAPH_COMPLETED, 0, 1), + VX_ERROR_NOT_SUPPORTED, "GRAPH_COMPLETED on a node rejected"); + EXPECT_STATUS(vxRegisterEvent((vx_reference)g.node, VX_EVENT_GRAPH_PARAMETER_CONSUMED, 0, 1), + VX_ERROR_NOT_SUPPORTED, "GRAPH_PARAMETER_CONSUMED on a node rejected"); + // [spec] "the application does NOT register user events using vxRegisterEvent." + EXPECT_STATUS(vxRegisterEvent((vx_reference)g.graph, VX_EVENT_USER, 0, 1), + VX_ERROR_NOT_SUPPORTED, "USER event registration rejected"); + // Events are reported for graphs and nodes, not for data objects. + EXPECT_STATUS(vxRegisterEvent((vx_reference)g.in, VX_EVENT_GRAPH_COMPLETED, 0, 1), + VX_ERROR_NOT_SUPPORTED, "registration on an image rejected"); + + // The parameter index only means something for GRAPH_PARAMETER_CONSUMED, and + // there it has to name a real graph parameter. + EXPECT_STATUS(vxRegisterEvent((vx_reference)g.graph, VX_EVENT_GRAPH_PARAMETER_CONSUMED, 99, 1), + VX_ERROR_INVALID_PARAMETERS, "out of range parameter index rejected"); + + // Valid registrations. + EXPECT_STATUS(vxRegisterEvent((vx_reference)g.graph, VX_EVENT_GRAPH_COMPLETED, 0, 100), + VX_SUCCESS, "GRAPH_COMPLETED on a graph accepted"); + EXPECT_STATUS(vxRegisterEvent((vx_reference)g.graph, VX_EVENT_GRAPH_PARAMETER_CONSUMED, 1, 101), + VX_SUCCESS, "GRAPH_PARAMETER_CONSUMED on a graph accepted"); + EXPECT_STATUS(vxRegisterEvent((vx_reference)g.node, VX_EVENT_NODE_COMPLETED, 0, 102), + VX_SUCCESS, "NODE_COMPLETED on a node accepted"); + EXPECT_STATUS(vxRegisterEvent((vx_reference)g.node, VX_EVENT_NODE_ERROR, 0, 103), + VX_SUCCESS, "NODE_ERROR on a node accepted"); + + // Registering the same thing again replaces the app_value rather than + // adding a second registration; the delivered value is checked in Test 5. + EXPECT_STATUS(vxRegisterEvent((vx_reference)g.graph, VX_EVENT_GRAPH_COMPLETED, 0, 200), + VX_SUCCESS, "re-registration accepted"); + + // vxRegisterGraphEvent is the graph-scoped spelling of the same call. + EXPECT_STATUS(vxRegisterGraphEvent((vx_reference)g.graph, VX_EVENT_GRAPH_COMPLETED, 0, 201), + VX_SUCCESS, "vxRegisterGraphEvent accepted"); + + // [spec] "This API MUST be called before doing vxVerifyGraph for that graph." + CHECK_STATUS(vxVerifyGraph(g.graph)); + EXPECT_STATUS(vxRegisterEvent((vx_reference)g.graph, VX_EVENT_GRAPH_COMPLETED, 0, 300), + VX_ERROR_NOT_SUPPORTED, "registration after verify rejected"); + EXPECT_STATUS(vxRegisterEvent((vx_reference)g.node, VX_EVENT_NODE_COMPLETED, 0, 301), + VX_ERROR_NOT_SUPPORTED, "node registration after verify rejected"); + + release_not_graph(g); + vxReleaseContext(&context); + return errors; +} + +// --------------------------------------------------------------------------- +// Test 5: events are reported only for references the application registered +// --------------------------------------------------------------------------- +static int test_event_delivery_gating() +{ + int errors = 0; + printf("\n=== Test 5: event delivery is limited to registrations ===\n"); + + vx_context context = vxCreateContext(); + set_event_timeout(context); + + // Two nodes, of which only the second is registered for completion events. + vx_graph graph = vxCreateGraph(context); + vx_image in = vxCreateImage(context, IMG_W, IMG_H, VX_DF_IMAGE_U8); + vx_image mid = vxCreateImage(context, IMG_W, IMG_H, VX_DF_IMAGE_U8); + vx_image out = vxCreateImage(context, IMG_W, IMG_H, VX_DF_IMAGE_U8); + vx_node n0 = vxNotNode(graph, in, mid); + vx_node n1 = vxNotNode(graph, mid, out); + CHECK_NOT_NULL(n0, "first vxNotNode"); + CHECK_NOT_NULL(n1, "second vxNotNode"); + CHECK_STATUS(fill_u8_image(in, 0xA5)); + + CHECK_STATUS(vxRegisterEvent((vx_reference)n1, VX_EVENT_NODE_COMPLETED, 0, 501)); + CHECK_STATUS(vxRegisterEvent((vx_reference)graph, VX_EVENT_GRAPH_COMPLETED, 0, 502)); + // Replaces the value above, so 503 is what should arrive. + CHECK_STATUS(vxRegisterEvent((vx_reference)graph, VX_EVENT_GRAPH_COMPLETED, 0, 503)); + + CHECK_STATUS(vxEnableEvents(context)); + CHECK_STATUS(vxVerifyGraph(graph)); + CHECK_STATUS(vxProcessGraph(graph)); + + EventTally t = drain_events(context); + printf(" INFO: %u event(s): graph_completed=%u node_completed=%u consumed=%u\n", + t.total, t.graph_completed, t.node_completed, t.parameter_consumed); + + // [spec] "This event is generated every time a graph execution completes." + EXPECT_TRUE(t.graph_completed == 1, "one GRAPH_COMPLETED for one execution"); + EXPECT_TRUE(t.node_error == 0, "no NODE_ERROR from a successful execution"); + // No graph parameters were configured for queueing here. + EXPECT_TRUE(t.parameter_consumed == 0, "no GRAPH_PARAMETER_CONSUMED without queueing"); + // n0 was never registered, so at most the one registered node may report. + // Graph optimization is free to rewrite the nodes, so the count is bounded + // rather than fixed; what matters is that the unregistered one stays silent. + EXPECT_TRUE(t.node_completed <= 1, "the unregistered node does not report completion"); + + // Every registration in this test used a distinct non-zero app_value, so an + // event carrying zero could only have come from a reference that was never + // registered, whatever internal node the optimizer produced for it. + bool saw_zero = false, saw_503 = false, saw_502 = false, saw_501 = false; + for (vx_uint32 v : t.app_values) { + if (v == 0) saw_zero = true; + if (v == 503) saw_503 = true; + if (v == 502) saw_502 = true; + if (v == 501) saw_501 = true; + } + EXPECT_TRUE(!saw_zero, "no events reported for unregistered references"); + EXPECT_TRUE(saw_503 && !saw_502, "re-registration replaced the graph app_value"); + if (t.node_completed) + EXPECT_TRUE(saw_501, "registered node event carries its app_value"); + + vxReleaseNode(&n0); + vxReleaseNode(&n1); + vxReleaseImage(&in); + vxReleaseImage(&mid); + vxReleaseImage(&out); + vxReleaseGraph(&graph); + vxReleaseContext(&context); + return errors; +} + +// --------------------------------------------------------------------------- +// Test 6: what vxDisableEvents does and does not affect +// --------------------------------------------------------------------------- +static int test_events_disabled_semantics() +{ + int errors = 0; + printf("\n=== Test 6: vxEnableEvents / vxDisableEvents semantics ===\n"); + + vx_context context = vxCreateContext(); + set_event_timeout(context); + + NotGraph g = make_not_graph(context, 0); + CHECK_STATUS(fill_u8_image(g.in, 0xA5)); + CHECK_STATUS(vxRegisterEvent((vx_reference)g.graph, VX_EVENT_GRAPH_COMPLETED, 0, 601)); + + EXPECT_STATUS(vxEnableEvents(context), VX_SUCCESS, "vxEnableEvents"); + CHECK_STATUS(vxVerifyGraph(g.graph)); + CHECK_STATUS(vxProcessGraph(g.graph)); + + // One event is now queued and has not been collected. + EXPECT_STATUS(vxDisableEvents(context), VX_SUCCESS, "vxDisableEvents"); + + // [spec] "any event generated before this API is called will still be + // returned via vxWaitEvent API." This uses the blocking form on purpose: + // requiring events to be enabled here would strand the queued event. + { + vx_event_t ev; + memset(&ev, 0, sizeof(ev)); + EXPECT_STATUS(vxWaitEvent(context, &ev, vx_false_e), VX_SUCCESS, + "blocking wait returns an event queued before disable"); + EXPECT_TRUE(ev.type == VX_EVENT_GRAPH_COMPLETED && ev.app_value == 601, + "the returned event is the one that was queued"); + } + + // [spec] "no additional events would be returned via vxWaitEvent API until + // events are enabled again." + CHECK_STATUS(vxProcessGraph(g.graph)); + { + vx_event_t ev; + memset(&ev, 0, sizeof(ev)); + EXPECT_TRUE(vxWaitEvent(context, &ev, vx_true_e) != VX_SUCCESS, + "no new events are delivered while disabled"); + } + + // Re-enabling resumes reporting. + EXPECT_STATUS(vxEnableEvents(context), VX_SUCCESS, "vxEnableEvents again"); + CHECK_STATUS(vxProcessGraph(g.graph)); + { + vx_event_t ev; + memset(&ev, 0, sizeof(ev)); + EXPECT_STATUS(vxWaitEvent(context, &ev, vx_false_e), VX_SUCCESS, + "events flow again after re-enabling"); + } + + // A non-blocking wait on an empty queue reports failure rather than blocking. + drain_events(context); + { + vx_event_t ev; + memset(&ev, 0, sizeof(ev)); + EXPECT_TRUE(vxWaitEvent(context, &ev, vx_true_e) != VX_SUCCESS, + "non-blocking wait on an empty queue fails"); + } + + EXPECT_STATUS(vxWaitEvent(context, nullptr, vx_true_e), VX_ERROR_INVALID_REFERENCE, + "wait with a null event structure rejected"); + EXPECT_STATUS(vxEnableEvents(nullptr), VX_ERROR_INVALID_REFERENCE, + "vxEnableEvents on a null context rejected"); + EXPECT_STATUS(vxDisableEvents(nullptr), VX_ERROR_INVALID_REFERENCE, + "vxDisableEvents on a null context rejected"); + + release_not_graph(g); + vxReleaseContext(&context); + return errors; +} + +// --------------------------------------------------------------------------- +// Test 7: user events +// --------------------------------------------------------------------------- +static int test_user_events() +{ + int errors = 0; + printf("\n=== Test 7: user events ===\n"); + + vx_context context = vxCreateContext(); + set_event_timeout(context); + vx_graph graph = vxCreateGraph(context); + + CHECK_STATUS(vxEnableEvents(context)); + + int payload = 4242; + // User events need no registration, unlike implementation-generated ones. + EXPECT_STATUS(vxSendUserEvent(context, 700, &payload), VX_SUCCESS, "vxSendUserEvent"); + + { + vx_event_t ev; + memset(&ev, 0, sizeof(ev)); + EXPECT_STATUS(vxWaitEvent(context, &ev, vx_false_e), VX_SUCCESS, "user event received"); + EXPECT_TRUE(ev.type == VX_EVENT_USER, "event type is VX_EVENT_USER"); + EXPECT_TRUE(ev.app_value == 700, "user event carries the given app_value"); + EXPECT_TRUE(ev.event_info.user_event.user_event_parameter == &payload, + "user event carries the given parameter"); + // [spec] timestamp is the time the event was generated, in nanoseconds. + EXPECT_TRUE(ev.timestamp != 0, "user event carries a timestamp"); + } + + // A null parameter is allowed; only the app_value is required. + EXPECT_STATUS(vxSendUserEvent(context, 701, nullptr), VX_SUCCESS, + "vxSendUserEvent with no parameter"); + drain_events(context); + + // The graph-scoped spelling reaches the same queue. + EXPECT_STATUS(vxSendUserGraphEvent(graph, 702, nullptr), VX_SUCCESS, "vxSendUserGraphEvent"); + { + vx_event_t ev; + memset(&ev, 0, sizeof(ev)); + EXPECT_STATUS(vxWaitGraphEvent(graph, &ev, vx_false_e), VX_SUCCESS, "vxWaitGraphEvent"); + EXPECT_TRUE(ev.app_value == 702, "graph-scoped user event carries its app_value"); + } + + EXPECT_STATUS(vxSendUserEvent(nullptr, 703, nullptr), VX_ERROR_INVALID_REFERENCE, + "vxSendUserEvent on a null context rejected"); + + // The graph-scoped enable/disable pair operates on the owning context. + EXPECT_STATUS(vxDisableGraphEvents(graph), VX_SUCCESS, "vxDisableGraphEvents"); + EXPECT_TRUE(vxSendUserEvent(context, 704, nullptr) != VX_SUCCESS, + "user events are not recorded while disabled"); + EXPECT_STATUS(vxEnableGraphEvents(graph), VX_SUCCESS, "vxEnableGraphEvents"); + EXPECT_STATUS(vxSendUserEvent(context, 705, nullptr), VX_SUCCESS, + "user events resume after re-enabling"); + drain_events(context); + + vxReleaseGraph(&graph); + vxReleaseContext(&context); + return errors; +} + +// --------------------------------------------------------------------------- +// Test 8: timeout and pipeline depth attributes +// --------------------------------------------------------------------------- +static int test_attributes() +{ + int errors = 0; + printf("\n=== Test 8: pipelining attributes ===\n"); + + vx_context context = vxCreateContext(); + NotGraph g = make_not_graph(context, 0); + + // [spec] the implementation shall initially set the timeouts to + // VX_TIMEOUT_WAIT_FOREVER. + { + vx_uint32 timeout = 0; + CHECK_STATUS(vxQueryContext(context, VX_CONTEXT_EVENT_TIMEOUT, &timeout, sizeof(timeout))); + EXPECT_TRUE(timeout == VX_TIMEOUT_WAIT_FOREVER, + "VX_CONTEXT_EVENT_TIMEOUT defaults to WAIT_FOREVER"); + timeout = 1234; + CHECK_STATUS(vxSetContextAttribute(context, VX_CONTEXT_EVENT_TIMEOUT, &timeout, sizeof(timeout))); + timeout = 0; + CHECK_STATUS(vxQueryContext(context, VX_CONTEXT_EVENT_TIMEOUT, &timeout, sizeof(timeout))); + EXPECT_TRUE(timeout == 1234, "VX_CONTEXT_EVENT_TIMEOUT round-trips"); + } + + { + vx_uint32 timeout = 0; + CHECK_STATUS(vxQueryGraph(g.graph, VX_GRAPH_TIMEOUT, &timeout, sizeof(timeout))); + EXPECT_TRUE(timeout == VX_TIMEOUT_WAIT_FOREVER, + "VX_GRAPH_TIMEOUT defaults to WAIT_FOREVER"); + timeout = 5678; + CHECK_STATUS(vxSetGraphAttribute(g.graph, VX_GRAPH_TIMEOUT, &timeout, sizeof(timeout))); + timeout = 0; + CHECK_STATUS(vxQueryGraph(g.graph, VX_GRAPH_TIMEOUT, &timeout, sizeof(timeout))); + EXPECT_TRUE(timeout == 5678, "VX_GRAPH_TIMEOUT round-trips"); + } + + { + vx_uint32 timeout = 0; + CHECK_STATUS(vxQueryGraph(g.graph, VX_GRAPH_EVENT_TIMEOUT, &timeout, sizeof(timeout))); + EXPECT_TRUE(timeout == VX_TIMEOUT_WAIT_FOREVER, + "VX_GRAPH_EVENT_TIMEOUT defaults to WAIT_FOREVER"); + timeout = 4321; + CHECK_STATUS(vxSetGraphAttribute(g.graph, VX_GRAPH_EVENT_TIMEOUT, &timeout, sizeof(timeout))); + timeout = 0; + CHECK_STATUS(vxQueryGraph(g.graph, VX_GRAPH_EVENT_TIMEOUT, &timeout, sizeof(timeout))); + EXPECT_TRUE(timeout == 4321, "VX_GRAPH_EVENT_TIMEOUT round-trips"); + } + + // The pipeline depth an implementation settles on is its own choice, so the + // value is only reported, not asserted. + { + vx_uint32 depth = 0; + CHECK_STATUS(vxQueryGraph(g.graph, VX_GRAPH_PIPELINE_DEPTH, &depth, sizeof(depth))); + printf(" INFO: VX_GRAPH_PIPELINE_DEPTH = %u\n", depth); + EXPECT_TRUE(depth >= 1, "pipeline depth is at least one"); + } + + release_not_graph(g); + vxReleaseContext(&context); + return errors; +} + +// --------------------------------------------------------------------------- +// Test 9: QUEUE_MANUAL end to end, including the data path +// --------------------------------------------------------------------------- +static int test_manual_queue_end_to_end() +{ + int errors = 0; + printf("\n=== Test 9: QUEUE_MANUAL end to end ===\n"); + + vx_context context = vxCreateContext(); + set_event_timeout(context); + NotGraph g = make_not_graph(context, 2); + + // Written before the parameters become queued, because a queued parameter is + // handed over through the queue rather than accessed directly. + CHECK_STATUS(fill_u8_image(g.in, 0xA5)); + + vx_reference in_ref = (vx_reference)g.in; + vx_reference out_ref = (vx_reference)g.out; + vx_graph_parameter_queue_params_t q[2]; + memset(q, 0, sizeof(q)); + q[0].graph_parameter_index = 0; + q[0].refs_list_size = 1; + q[0].refs_list = &in_ref; + q[1].graph_parameter_index = 1; + q[1].refs_list_size = 1; + q[1].refs_list = &out_ref; + + CHECK_STATUS(vxRegisterEvent((vx_reference)g.graph, VX_EVENT_GRAPH_COMPLETED, 0, 900)); + CHECK_STATUS(vxRegisterEvent((vx_reference)g.graph, VX_EVENT_GRAPH_PARAMETER_CONSUMED, 1, 901)); + CHECK_STATUS(vxEnableEvents(context)); + + EXPECT_STATUS(vxSetGraphScheduleConfig(g.graph, VX_GRAPH_SCHEDULE_MODE_QUEUE_MANUAL, 2, q), + VX_SUCCESS, "QUEUE_MANUAL configured"); + + // Bounds the dequeue below so a lost wake-up fails instead of hanging. + { + vx_uint32 timeout = WAIT_TIMEOUT_MS; + CHECK_STATUS(vxSetGraphAttribute(g.graph, VX_GRAPH_TIMEOUT, &timeout, sizeof(timeout))); + } + EXPECT_STATUS(vxVerifyGraph(g.graph), VX_SUCCESS, "vxVerifyGraph"); + + EXPECT_STATUS(vxGraphParameterEnqueueReadyRef(g.graph, 0, &in_ref, 1), + VX_SUCCESS, "input enqueued"); + EXPECT_STATUS(vxGraphParameterEnqueueReadyRef(g.graph, 1, &out_ref, 1), + VX_SUCCESS, "output enqueued"); + + EXPECT_STATUS(vxScheduleGraph(g.graph), VX_SUCCESS, "vxScheduleGraph"); + EXPECT_STATUS(vxWaitGraph(g.graph), VX_SUCCESS, "vxWaitGraph"); + + { + vx_reference deq = nullptr; + vx_uint32 num = 0; + EXPECT_STATUS(vxGraphParameterDequeueDoneRef(g.graph, 1, &deq, 1, &num), + VX_SUCCESS, "output dequeued"); + EXPECT_TRUE(num == 1 && deq == out_ref, "the dequeued reference is the one enqueued"); + } + + // NOT of 0xA5 is 0x5A. This is what proves the queued references, and not + // the defaults bound at verify time, carried the data through the graph. + EXPECT_TRUE(u8_image_is_uniform(g.out, 0x5A), "queued output holds the computed result"); + + { + EventTally t = drain_events(context); + printf(" INFO: %u event(s): graph_completed=%u consumed=%u\n", + t.total, t.graph_completed, t.parameter_consumed); + EXPECT_TRUE(t.graph_completed == 1, "one GRAPH_COMPLETED for one execution"); + // [spec] generated when a data reference at a graph parameter is consumed. + EXPECT_TRUE(t.parameter_consumed >= 1, "the consumed output parameter reported"); + } + + release_not_graph(g); + vxReleaseContext(&context); + return errors; +} + +// --------------------------------------------------------------------------- +// Test 10: streaming +// --------------------------------------------------------------------------- +static int test_streaming() +{ + int errors = 0; + printf("\n=== Test 10: graph streaming ===\n"); + + vx_context context = vxCreateContext(); + NotGraph g = make_not_graph(context, 0); + CHECK_STATUS(fill_u8_image(g.in, 0xA5)); + + EXPECT_STATUS(vxEnableGraphStreaming(nullptr, nullptr), VX_ERROR_INVALID_REFERENCE, + "vxEnableGraphStreaming on a null graph rejected"); + + // [spec] "This function must be called before vxVerifyGraph." The trigger + // node is optional, so a null one is allowed. + EXPECT_STATUS(vxEnableGraphStreaming(g.graph, g.node), VX_SUCCESS, + "vxEnableGraphStreaming with a trigger node"); + + // Streaming cannot start until the graph has been verified. + EXPECT_STATUS(vxStartGraphStreaming(g.graph), VX_ERROR_NOT_SUFFICIENT, + "start before verify reports NOT_SUFFICIENT"); + + CHECK_STATUS(vxVerifyGraph(g.graph)); + EXPECT_STATUS(vxStartGraphStreaming(g.graph), VX_SUCCESS, "vxStartGraphStreaming"); + EXPECT_STATUS(vxStopGraphStreaming(g.graph), VX_SUCCESS, "vxStopGraphStreaming"); + + // Stopping leaves streaming disabled, so it cannot simply be started again. + // Re-enabling would have to happen before verification, as above. + EXPECT_TRUE(vxStartGraphStreaming(g.graph) != VX_SUCCESS, + "starting again after a stop does not report success"); + EXPECT_STATUS(vxStopGraphStreaming(nullptr), VX_ERROR_INVALID_REFERENCE, + "vxStopGraphStreaming on a null graph rejected"); + EXPECT_STATUS(vxStartGraphStreaming(nullptr), VX_ERROR_INVALID_REFERENCE, + "vxStartGraphStreaming on a null graph rejected"); + + release_not_graph(g); + vxReleaseContext(&context); + return errors; +} + +// --------------------------------------------------------------------------- +// Test 11: QUEUE_AUTO configuration +// --------------------------------------------------------------------------- +static int test_queue_auto() +{ + int errors = 0; + printf("\n=== Test 11: QUEUE_AUTO ===\n"); + + vx_context context = vxCreateContext(); + set_event_timeout(context); + NotGraph g = make_not_graph(context, 2); + CHECK_STATUS(fill_u8_image(g.in, 0x0F)); + + vx_reference in_ref = (vx_reference)g.in; + vx_reference out_ref = (vx_reference)g.out; + vx_graph_parameter_queue_params_t q[2]; + memset(q, 0, sizeof(q)); + q[0].graph_parameter_index = 0; + q[0].refs_list_size = 1; + q[0].refs_list = &in_ref; + q[1].graph_parameter_index = 1; + q[1].refs_list_size = 1; + q[1].refs_list = &out_ref; + + EXPECT_STATUS(vxSetGraphScheduleConfig(g.graph, VX_GRAPH_SCHEDULE_MODE_QUEUE_AUTO, 2, q), + VX_SUCCESS, "QUEUE_AUTO configured"); + { + vx_enum mode = 0; + CHECK_STATUS(vxQueryGraph(g.graph, VX_GRAPH_SCHEDULE_MODE, &mode, sizeof(mode))); + EXPECT_TRUE(mode == VX_GRAPH_SCHEDULE_MODE_QUEUE_AUTO, + "VX_GRAPH_SCHEDULE_MODE reports QUEUE_AUTO"); + } + { + vx_uint32 timeout = WAIT_TIMEOUT_MS; + CHECK_STATUS(vxSetGraphAttribute(g.graph, VX_GRAPH_TIMEOUT, &timeout, sizeof(timeout))); + } + EXPECT_STATUS(vxVerifyGraph(g.graph), VX_SUCCESS, "vxVerifyGraph"); + + // In QUEUE_AUTO the implementation schedules as soon as a full set of + // references is available, with no vxScheduleGraph from the application. + EXPECT_STATUS(vxGraphParameterEnqueueReadyRef(g.graph, 0, &in_ref, 1), + VX_SUCCESS, "input enqueued"); + EXPECT_STATUS(vxGraphParameterEnqueueReadyRef(g.graph, 1, &out_ref, 1), + VX_SUCCESS, "output enqueued"); + + { + vx_reference deq = nullptr; + vx_uint32 num = 0; + EXPECT_STATUS(vxGraphParameterDequeueDoneRef(g.graph, 1, &deq, 1, &num), + VX_SUCCESS, "output dequeued without an explicit schedule"); + EXPECT_TRUE(num == 1 && deq == out_ref, "the dequeued reference is the one enqueued"); + } + EXPECT_TRUE(u8_image_is_uniform(g.out, 0xF0), "queued output holds the computed result"); + + // A call made after verify may only supply refs_list; everything else has to + // stay as it was, so switching the mode now is not a legal reconfiguration. + EXPECT_STATUS(vxSetGraphScheduleConfig(g.graph, VX_GRAPH_SCHEDULE_MODE_NORMAL, 0, nullptr), + VX_ERROR_INVALID_PARAMETERS, "mode change after verify rejected"); + + release_not_graph(g); + vxReleaseContext(&context); + return errors; +} + +// --------------------------------------------------------------------------- +// main +// --------------------------------------------------------------------------- +int main() +{ + printf("OpenVX Graph Pipelining Extension API Coverage Test\n"); + printf("===================================================\n"); + + // The extension can be compiled out, in which case every entry point reports + // VX_ERROR_NOT_SUPPORTED and there is nothing here to check. + { + vx_context probe = vxCreateContext(); + if (!probe) { + printf("FATAL: vxCreateContext failed\n"); + return 1; + } + vx_graph graph = vxCreateGraph(probe); + vx_status s = vxSetGraphScheduleConfig(graph, VX_GRAPH_SCHEDULE_MODE_NORMAL, 0, nullptr); + vxReleaseGraph(&graph); + vxReleaseContext(&probe); + if (s == VX_ERROR_NOT_SUPPORTED) { + printf("SKIP: built without the pipelining extension\n"); + return 0; + } + } + + int total_errors = 0; + total_errors += test_schedule_config_validation(); + total_errors += test_refs_list_after_verify(); + total_errors += test_queue_validation(); + total_errors += test_event_registration_validation(); + total_errors += test_event_delivery_gating(); + total_errors += test_events_disabled_semantics(); + total_errors += test_user_events(); + total_errors += test_attributes(); + total_errors += test_manual_queue_end_to_end(); + total_errors += test_streaming(); + total_errors += test_queue_auto(); + + printf("\n===================================================\n"); + if (total_errors == 0) { + printf("RESULT: ALL TESTS PASSED\n"); + } else { + printf("RESULT: %d ERROR(S) DETECTED\n", total_errors); + } + return (total_errors == 0) ? 0 : 1; +} From 8a8b36539dc78a8a9e3c95a75e89ed0d3864310c Mon Sep 17 00:00:00 2001 From: KiritiGowda Date: Mon, 3 Aug 2026 20:51:25 -0700 Subject: [PATCH 17/19] fix(pipelining): guard a queue's reference list with the queue lock vxSetGraphScheduleConfig may be called after verify to hand over the references a graph parameter queue accepts, and vxAddReferencesToGraphParameterList appends to that same list. Neither took the queue lock, while vxGraphParameterEnqueueReadyRef searched the list without it, so an application enqueueing on one thread while handing over references on another could see the vector reallocated mid-search. valid_refs is now only read or written under the queue's mutex. The enqueue path takes that lock once around the whole check-and-push instead of per reference, and drops it before signalling enqueue_cv: the QUEUE_AUTO executor waits on that condition with a predicate that takes the queue lock, so holding the two in the other order would deadlock against it. The lock ordering that makes the queue mutex the innermost lock is written down on the struct. Also adds the missing null-pointer check to vxSetContextAttribute(VX_CONTEXT_EVENT_TIMEOUT). Unlike the other attribute entry points, that function has no blanket ptr guard and relies on each case checking, so passing NULL dereferenced it. The API test covers both: a bounded two-thread case that enqueues while another thread replaces the reference list, and null pointers for the attributes this extension adds. Co-authored-by: Cursor --- amd_openvx/openvx/ago/ago_internal.h | 5 + amd_openvx/openvx/api/vx_api.cpp | 1 + amd_openvx/openvx/api/vx_pipelining_api.cpp | 59 ++++++----- .../pipelining_api/CMakeLists.txt | 5 +- .../pipelining_api/pipelining_api.cpp | 98 +++++++++++++++++++ 5 files changed, 144 insertions(+), 24 deletions(-) diff --git a/amd_openvx/openvx/ago/ago_internal.h b/amd_openvx/openvx/ago/ago_internal.h index 75ea77449..4929040c8 100644 --- a/amd_openvx/openvx/ago/ago_internal.h +++ b/amd_openvx/openvx/ago/ago_internal.h @@ -668,6 +668,11 @@ struct AgoNodeList { AgoNode * tail; AgoNode * trash; }; +// mtx guards every field below it, including valid_refs, which the application +// can replace after verify while an executor thread is reading it. It is the +// innermost lock in the pipelining paths -- graph->cs, context->cs and +// enqueue_mtx are all taken before it -- so no other lock may be acquired while +// it is held. struct AgoGraphParameterQueue { std::mutex mtx; std::condition_variable done_cv; diff --git a/amd_openvx/openvx/api/vx_api.cpp b/amd_openvx/openvx/api/vx_api.cpp index 5288fa9be..fb0588ddf 100644 --- a/amd_openvx/openvx/api/vx_api.cpp +++ b/amd_openvx/openvx/api/vx_api.cpp @@ -362,6 +362,7 @@ VX_API_ENTRY vx_status VX_API_CALL vxSetContextAttribute(vx_context context, vx_ switch (attribute) { case VX_CONTEXT_EVENT_TIMEOUT: + if(!ptr) return VX_ERROR_INVALID_PARAMETERS; if (size == sizeof(vx_uint32)) { AgoContextEventSystem * evsys = agoGetContextEventSystem(context); if (evsys) { diff --git a/amd_openvx/openvx/api/vx_pipelining_api.cpp b/amd_openvx/openvx/api/vx_pipelining_api.cpp index 28c342754..ddf0fdcf8 100644 --- a/amd_openvx/openvx/api/vx_pipelining_api.cpp +++ b/amd_openvx/openvx/api/vx_pipelining_api.cpp @@ -71,6 +71,7 @@ static vx_status updateGraphScheduleRefsList( } // Built separately so a bad entry late in the list leaves the queue's // existing references untouched. + std::lock_guard qlock(q->mtx); q->valid_refs = std::move(refs); } return VX_SUCCESS; @@ -137,8 +138,10 @@ VX_API_ENTRY vx_status VX_API_CALL vxSetGraphScheduleConfig( // list, but that is a requirement on the application and it is not // policed here: GraphPipeline.ScalarOutput configures index 1 twice // (leaving index 2 unconfigured) and must still be accepted. - pipe->param_queues[index].get()->max_depth = p.refs_list_size; - pipe->param_queues[index].get()->enabled = true; + AgoGraphParameterQueue * q = pipe->param_queues[index].get(); + std::lock_guard qlock(q->mtx); + q->max_depth = p.refs_list_size; + q->enabled = true; if (p.refs_list) { for (vx_uint32 j = 0; j < p.refs_list_size; j++) { vx_reference ref = p.refs_list[j]; @@ -146,7 +149,7 @@ VX_API_ENTRY vx_status VX_API_CALL vxSetGraphScheduleConfig( return VX_ERROR_INVALID_PARAMETERS; if (!agoIsValidReference((AgoReference *)ref)) return VX_ERROR_INVALID_REFERENCE; - pipe->param_queues[index].get()->valid_refs.push_back((AgoData *)ref); + q->valid_refs.push_back((AgoData *)ref); } } } @@ -172,6 +175,7 @@ VX_API_ENTRY vx_status VX_API_CALL vxGetGraphParameterRefsList( AgoGraphPipeliningState * pipe = agoGetGraphPipeliningState(graph); if (pipe && param < (vx_uint32)pipe->param_queues.size() && refs_list) { AgoGraphParameterQueue * q = pipe->param_queues[param].get(); + std::lock_guard qlock(q->mtx); if (ref_list_size >= (vx_uint32)q->valid_refs.size()) { for (size_t i = 0; i < q->valid_refs.size(); i++) { refs_list[i] = (vx_reference)q->valid_refs[i]; @@ -197,6 +201,7 @@ VX_API_ENTRY vx_status VX_API_CALL vxAddReferencesToGraphParameterList( AgoGraphPipeliningState * pipe = agoGetGraphPipeliningState(graph); if (pipe && graph_parameter_index < (vx_uint32)pipe->param_queues.size()) { AgoGraphParameterQueue * q = pipe->param_queues[graph_parameter_index].get(); + std::lock_guard qlock(q->mtx); for (vx_uint32 i = 0; i < number_to_add; i++) { if (!new_references[i] || !agoIsValidReference((AgoReference *)new_references[i])) return VX_ERROR_INVALID_REFERENCE; @@ -242,29 +247,37 @@ VX_API_ENTRY vx_status VX_API_CALL vxGraphParameterEnqueueReadyRef( q->max_depth = 0; } - for (vx_uint32 i = 0; i < num_refs; i++) { - if (!refs[i] || !agoIsValidReference((AgoReference *)refs[i])) - return VX_ERROR_INVALID_REFERENCE; - // If valid_refs is configured, reject refs not in the list. - if (!q->valid_refs.empty()) { - bool found = false; - for (AgoData * valid : q->valid_refs) { - if ((vx_reference)valid == refs[i]) { - found = true; - break; + { + // The whole check-and-push is under the queue lock, so a concurrent + // vxSetGraphScheduleConfig or vxAddReferencesToGraphParameterList + // cannot swap valid_refs out from under the search below. The lock is + // released before enqueue_mtx is taken: the QUEUE_AUTO executor waits + // on enqueue_cv with a predicate that takes q->mtx, so acquiring the + // two in the other order here would deadlock against it. + std::lock_guard lock(q->mtx); + for (vx_uint32 i = 0; i < num_refs; i++) { + if (!refs[i] || !agoIsValidReference((AgoReference *)refs[i])) + return VX_ERROR_INVALID_REFERENCE; + // If valid_refs is configured, reject refs not in the list. + if (!q->valid_refs.empty()) { + bool found = false; + for (AgoData * valid : q->valid_refs) { + if ((vx_reference)valid == refs[i]) { + found = true; + break; + } } + if (!found) + return VX_ERROR_INVALID_PARAMETERS; } - if (!found) - return VX_ERROR_INVALID_PARAMETERS; + // refs_list_size given at schedule-config time is the queue depth. + // Counting only the refs still waiting to be picked up keeps this a + // limit on what the application has handed over but the graph has + // not yet taken, which is what the depth is there to bound. + if (q->max_depth && q->ready_refs.size() >= (size_t)q->max_depth) + return VX_ERROR_NO_RESOURCES; + q->ready_refs.push_back((AgoData *)refs[i]); } - std::lock_guard lock(q->mtx); - // refs_list_size given at schedule-config time is the queue depth. - // Counting only the refs still waiting to be picked up keeps this a - // limit on what the application has handed over but the graph has - // not yet taken, which is what the depth is there to bound. - if (q->max_depth && q->ready_refs.size() >= (size_t)q->max_depth) - return VX_ERROR_NO_RESOURCES; - q->ready_refs.push_back((AgoData *)refs[i]); } { std::lock_guard lock(pipe->enqueue_mtx); diff --git a/tests/openvx_api_tests/pipelining_api/CMakeLists.txt b/tests/openvx_api_tests/pipelining_api/CMakeLists.txt index babcce9b6..599aa133f 100644 --- a/tests/openvx_api_tests/pipelining_api/CMakeLists.txt +++ b/tests/openvx_api_tests/pipelining_api/CMakeLists.txt @@ -49,4 +49,7 @@ include_directories (${ROCM_PATH}/include/mivisionx) link_directories (${ROCM_PATH}/lib) add_executable(openvx_pipelining_api pipelining_api.cpp) -target_link_libraries(${PROJECT_NAME} openvx) + +# The queueing tests drive the API from more than one thread. +find_package(Threads REQUIRED) +target_link_libraries(${PROJECT_NAME} openvx Threads::Threads) diff --git a/tests/openvx_api_tests/pipelining_api/pipelining_api.cpp b/tests/openvx_api_tests/pipelining_api/pipelining_api.cpp index b48c58117..ef0c97d55 100644 --- a/tests/openvx_api_tests/pipelining_api/pipelining_api.cpp +++ b/tests/openvx_api_tests/pipelining_api/pipelining_api.cpp @@ -36,6 +36,8 @@ THE SOFTWARE. #include #include #include +#include +#include #include #include #include @@ -779,6 +781,20 @@ static int test_attributes() EXPECT_TRUE(depth >= 1, "pipeline depth is at least one"); } + // A null pointer has to be rejected rather than dereferenced, for the + // attributes this extension adds as much as for any other. + { + vx_uint32 timeout = 100; + EXPECT_STATUS(vxSetContextAttribute(context, VX_CONTEXT_EVENT_TIMEOUT, nullptr, sizeof(timeout)), + VX_ERROR_INVALID_PARAMETERS, "set VX_CONTEXT_EVENT_TIMEOUT with null ptr"); + EXPECT_STATUS(vxQueryContext(context, VX_CONTEXT_EVENT_TIMEOUT, nullptr, sizeof(timeout)), + VX_ERROR_INVALID_PARAMETERS, "query VX_CONTEXT_EVENT_TIMEOUT with null ptr"); + EXPECT_STATUS(vxSetGraphAttribute(g.graph, VX_GRAPH_TIMEOUT, nullptr, sizeof(timeout)), + VX_ERROR_INVALID_PARAMETERS, "set VX_GRAPH_TIMEOUT with null ptr"); + EXPECT_STATUS(vxQueryGraph(g.graph, VX_GRAPH_PIPELINE_DEPTH, nullptr, sizeof(timeout)), + VX_ERROR_INVALID_PARAMETERS, "query VX_GRAPH_PIPELINE_DEPTH with null ptr"); + } + release_not_graph(g); vxReleaseContext(&context); return errors; @@ -965,6 +981,87 @@ static int test_queue_auto() return errors; } +// --------------------------------------------------------------------------- +// Test 12: handing over references while another thread enqueues +// +// A post-verify vxSetGraphScheduleConfig replaces the reference list a queue +// accepts, and the application is free to enqueue from a different thread at the +// same time. Nothing here blocks, so a failure shows up as a wrong status or a +// crash rather than a hung test -- and if the queue lock were ever taken around +// the enqueue notification, this would deadlock against the QUEUE_AUTO executor. +// --------------------------------------------------------------------------- +static int test_concurrent_refs_handover() +{ + int errors = 0; + printf("\n=== Test 12: concurrent refs_list handover ===\n"); + + const int ITERATIONS = 200; + + vx_context context = vxCreateContext(); + set_event_timeout(context); + NotGraph g = make_not_graph(context, 2); + CHECK_STATUS(fill_u8_image(g.in, 0x11)); + + vx_reference in_ref = (vx_reference)g.in; + vx_reference out_ref = (vx_reference)g.out; + vx_graph_parameter_queue_params_t q[2]; + memset(q, 0, sizeof(q)); + q[0].graph_parameter_index = 0; + q[0].refs_list_size = 1; + q[0].refs_list = &in_ref; + q[1].graph_parameter_index = 1; + q[1].refs_list_size = 1; + q[1].refs_list = &out_ref; + + CHECK_STATUS(vxSetGraphScheduleConfig(g.graph, VX_GRAPH_SCHEDULE_MODE_QUEUE_AUTO, 2, q)); + { + vx_uint32 timeout = WAIT_TIMEOUT_MS; + CHECK_STATUS(vxSetGraphAttribute(g.graph, VX_GRAPH_TIMEOUT, &timeout, sizeof(timeout))); + } + EXPECT_STATUS(vxVerifyGraph(g.graph), VX_SUCCESS, "vxVerifyGraph"); + + std::atomic handover_failures(0); + std::thread handover([&]() { + for (int i = 0; i < ITERATIONS; i++) { + if (vxSetGraphScheduleConfig(g.graph, VX_GRAPH_SCHEDULE_MODE_QUEUE_AUTO, 2, q) != VX_SUCCESS) + handover_failures++; + vx_reference seen[4] = { nullptr, nullptr, nullptr, nullptr }; + if (vxGetGraphParameterRefsList(g.graph, 0, 4, seen) != VX_SUCCESS) + handover_failures++; + if (vxAddReferencesToGraphParameterList(g.graph, 0, 1, &in_ref) != VX_SUCCESS) + handover_failures++; + } + }); + + int bad_status = 0; + for (int i = 0; i < ITERATIONS; i++) { + // The reference stays in the list throughout, so the only outcomes are + // acceptance or a full queue -- never a rejection of the reference. + vx_status si = vxGraphParameterEnqueueReadyRef(g.graph, 0, &in_ref, 1); + vx_status so = vxGraphParameterEnqueueReadyRef(g.graph, 1, &out_ref, 1); + if (si != VX_SUCCESS && si != VX_ERROR_NO_RESOURCES) bad_status++; + if (so != VX_SUCCESS && so != VX_ERROR_NO_RESOURCES) bad_status++; + + // Drain whatever the executor finished, without ever waiting for it. + for (vx_uint32 p = 0; p < 2; p++) { + vx_uint32 done = 0; + if (vxGraphParameterCheckDoneRef(g.graph, p, &done) == VX_SUCCESS && done > 0) { + vx_reference deq = nullptr; + vx_uint32 num = 0; + vxGraphParameterDequeueDoneRef(g.graph, p, &deq, 1, &num); + } + } + } + handover.join(); + + EXPECT_TRUE(bad_status == 0, "enqueue during handover never rejects a listed reference"); + EXPECT_TRUE(handover_failures.load() == 0, "refs_list handover succeeds alongside enqueueing"); + + release_not_graph(g); + vxReleaseContext(&context); + return errors; +} + // --------------------------------------------------------------------------- // main // --------------------------------------------------------------------------- @@ -1003,6 +1100,7 @@ int main() total_errors += test_manual_queue_end_to_end(); total_errors += test_streaming(); total_errors += test_queue_auto(); + total_errors += test_concurrent_refs_handover(); printf("\n===================================================\n"); if (total_errors == 0) { From 5910fe5f098c51bd96ae2241b5dad10e39642f94 Mon Sep 17 00:00:00 2001 From: KiritiGowda Date: Tue, 4 Aug 2026 00:43:16 -0700 Subject: [PATCH 18/19] fix(pipelining): stop a wait that never returns and an executor that spins vxWaitGraph on a graph with streaming enabled but never started waited for a stop request that nobody was going to make, so it never returned. Wait only when the streaming thread is actually running. The QUEUE_AUTO executor woke whenever any queue had a ready reference, while it only runs the graph once every enabled queue has one, so it spun at full speed between partial enqueues: the conformance and API runs took 378M loop iterations for 225k executions. Waiting on the condition the loop actually needs brings that to 641k iterations for 112k executions. Also drop code that nothing can reach. agoGraphHasNodeEventRegistrations and agoIsPipeliningGraph have no callers, and the event system that agoGetContextEventSystem created on demand is already created with the context, so that branch could only run during teardown, where it would resurrect what the destructor had just deleted. Co-authored-by: Cursor --- amd_openvx/openvx/ago/ago_interface.cpp | 17 +++--- amd_openvx/openvx/ago/ago_internal.h | 1 - amd_openvx/openvx/ago/ago_pipelining.cpp | 68 ++++++------------------ 3 files changed, 27 insertions(+), 59 deletions(-) diff --git a/amd_openvx/openvx/ago/ago_interface.cpp b/amd_openvx/openvx/ago/ago_interface.cpp index 0b5c24888..8e7a90a2f 100644 --- a/amd_openvx/openvx/ago/ago_interface.cpp +++ b/amd_openvx/openvx/ago/ago_interface.cpp @@ -3015,14 +3015,19 @@ int agoWaitGraph(AgoGraph * graph) if (graph->pipelining) { AgoGraphPipeliningState * pipe = graph->pipelining; // Streaming graphs are driven by the streaming thread; vxWaitGraph just - // needs to observe that no execution is active. + // needs to observe that no execution is active. There is only something + // to wait for once that thread is actually running: a graph with + // streaming enabled but never started has no execution in flight, and + // waiting for a stop that nobody will request would never return. if (pipe->streaming_enabled) { - while (!pipe->streaming_stop.load()) { - std::this_thread::sleep_for(std::chrono::milliseconds(1)); + if (pipe->streaming_thread.joinable()) { + while (!pipe->streaming_stop.load()) { + std::this_thread::sleep_for(std::chrono::milliseconds(1)); + } + // wait for streaming thread to finish + if (pipe->streaming_thread.joinable()) + pipe->streaming_thread.join(); } - // wait for streaming thread to finish - if (pipe->streaming_thread.joinable()) - pipe->streaming_thread.join(); return status; } // QUEUE_AUTO: stop the background executor, drain any refs that were diff --git a/amd_openvx/openvx/ago/ago_internal.h b/amd_openvx/openvx/ago/ago_internal.h index 4929040c8..e3ba5af8e 100644 --- a/amd_openvx/openvx/ago/ago_internal.h +++ b/amd_openvx/openvx/ago/ago_internal.h @@ -1004,7 +1004,6 @@ void agoNotifyGraphCompleted(AgoGraph * graph); void agoNotifyNodeCompleted(AgoGraph * graph, AgoNode * node); void agoNotifyNodeError(AgoGraph * graph, AgoNode * node, vx_status status); void agoNotifyGraphParameterConsumed(AgoGraph * graph, vx_uint32 graph_parameter_index); -bool agoGraphHasNodeEventRegistrations(AgoGraph * graph); vx_uint32 agoGetReferenceEnqueueCount(AgoContext * context, AgoReference * ref); void agoRemoveEventRegistrations(AgoContext * context, vx_reference ref); vx_uint64 agoEventTimestampNs(); diff --git a/amd_openvx/openvx/ago/ago_pipelining.cpp b/amd_openvx/openvx/ago/ago_pipelining.cpp index ec6be320a..65c285ec5 100644 --- a/amd_openvx/openvx/ago/ago_pipelining.cpp +++ b/amd_openvx/openvx/ago/ago_pipelining.cpp @@ -43,14 +43,13 @@ AgoGraphPipeliningState * agoGetGraphPipeliningState(AgoGraph * graph) return graph->pipelining; } +// The context owns its event system for its whole lifetime: it is created with +// the context and cleared when the context is destroyed. Creating one on demand +// here would resurrect it during teardown, so callers get whatever the context +// has and treat null as "no longer available". AgoContextEventSystem * agoGetContextEventSystem(AgoContext * context) { - if (!context) - return nullptr; - if (!context->events) { - context->events = new AgoContextEventSystem(); - } - return context->events; + return context ? context->events : nullptr; } static void agoStopGraphPipeliningExecutor(AgoGraph * graph) @@ -88,32 +87,6 @@ static vx_uint64 agoCurrentTimestampNs() return (vx_uint64)ns; } -bool agoGraphHasNodeEventRegistrations(AgoGraph * graph) -{ - if (!graph || !graph->ref.context) - return false; - AgoContextEventSystem * evsys = agoGetContextEventSystem(graph->ref.context); - if (!evsys) - return false; - std::lock_guard lock(evsys->registrations_mtx); - for (const auto& reg : evsys->registrations) { - if (reg.event_type == VX_EVENT_NODE_COMPLETED || reg.event_type == VX_EVENT_NODE_ERROR) { - AgoReference * r = (AgoReference *)reg.ref; - if (r && r->type == VX_TYPE_NODE && r->scope == (vx_reference)graph) - return true; - } - } - return false; -} - -static bool agoIsPipeliningGraph(AgoGraph * graph) -{ - AgoGraphPipeliningState * pipe = graph ? graph->pipelining : nullptr; - if (!pipe) - return false; - return pipe->schedule_mode != VX_GRAPH_SCHEDULE_MODE_NORMAL || pipe->streaming_enabled; -} - static AgoGraphParameterQueue * agoGetGraphParameterQueue(AgoGraphPipeliningState * pipe, vx_uint32 index) { if (!pipe || index >= pipe->param_queues.size()) @@ -650,15 +623,19 @@ static void agoGraphQueueAutoExecutorLoop(AgoGraph * graph) if (!pipe) return; - auto anyReady = [&pipe]() -> bool { + // An execution needs a ready reference at every enabled queue, so that is the + // condition to wait for as well. Waking on a single ready reference instead + // would return immediately and spin at full speed for as long as the + // application has enqueued to some graph parameters but not yet to all. + auto allReady = [&pipe]() -> bool { for (auto& q : pipe->param_queues) { if (!q->enabled) continue; std::lock_guard qlock(q->mtx); - if (!q->ready_refs.empty()) - return true; + if (q->ready_refs.empty()) + return false; } - return false; + return true; }; while (!pipe->executor_stop.load()) { @@ -666,27 +643,15 @@ static void agoGraphQueueAutoExecutorLoop(AgoGraph * graph) CAgoLock lock(graph->cs); if (pipe->schedule_mode != VX_GRAPH_SCHEDULE_MODE_QUEUE_AUTO) break; - - // Wait until all enabled queues have at least one ready ref. - bool all_ready = true; - for (auto& q : pipe->param_queues) { - if (!q->enabled) - continue; - std::lock_guard qlock(q->mtx); - if (q->ready_refs.empty()) { - all_ready = false; - break; - } - } - if (all_ready) { + if (allReady()) { agoExecutePipelinedGraphOnce(graph); continue; } } // Nothing to do: block until a ref is enqueued or we're asked to stop. std::unique_lock elock(pipe->enqueue_mtx); - pipe->enqueue_cv.wait_for(elock, std::chrono::milliseconds(1), [&pipe, &anyReady]() { - return pipe->executor_stop.load() || anyReady(); + pipe->enqueue_cv.wait_for(elock, std::chrono::milliseconds(1), [&pipe, &allReady]() { + return pipe->executor_stop.load() || allReady(); }); } } @@ -774,7 +739,6 @@ void agoStartGraphStreamingThread(AgoGraph * graph) AgoGraphPipeliningState * agoGetGraphPipeliningState(AgoGraph *) { return nullptr; } AgoContextEventSystem * agoGetContextEventSystem(AgoContext *) { return nullptr; } void agoStopGraphPipelining(AgoGraph *) {} -bool agoGraphHasNodeEventRegistrations(AgoGraph *) { return false; } void agoPushEvent(AgoContext *, const AgoEvent&) {} void agoNotifyGraphCompleted(AgoGraph *) {} void agoNotifyNodeCompleted(AgoGraph *, AgoNode *) {} From 9383a1c8ffcfff3946ab1865a6d38ca934c9714b Mon Sep 17 00:00:00 2001 From: KiritiGowda Date: Tue, 4 Aug 2026 00:43:38 -0700 Subject: [PATCH 19/19] test(pipelining): cover every entry point and the paths the CTS misses Measured against an instrumented build, the conformance suite reaches 70% of the lines in the two pipelining sources and enters only 80% of their functions, so most of the argument validation and error reporting the specification describes was never executed by any test. These thirteen tests take the two files to 92% of lines and 100% of functions, and call all twenty of the extension's entry points rather than nineteen. They cover what a post-verify schedule config may and may not change, the arguments of every queue and event call, both timeouts expiring, a user node that fails and the node error it has to report, the enqueue count through a whole cycle, an explicit schedule under QUEUE_AUTO, a graph released while its streaming thread is still running, and a graph parameter that was never queued. Two of them pin down behaviour worth not changing by accident: waiting on a graph whose streaming never started has to return, and an ROI keeps reading the image it was created over rather than following whatever was last enqueued. Every wait here is bounded, and a watchdog now names the test that hung instead of leaving a job to sit until it is killed. Co-authored-by: Cursor --- .../pipelining_api/pipelining_api.cpp | 993 +++++++++++++++++- 1 file changed, 983 insertions(+), 10 deletions(-) diff --git a/tests/openvx_api_tests/pipelining_api/pipelining_api.cpp b/tests/openvx_api_tests/pipelining_api/pipelining_api.cpp index ef0c97d55..0fef27fe4 100644 --- a/tests/openvx_api_tests/pipelining_api/pipelining_api.cpp +++ b/tests/openvx_api_tests/pipelining_api/pipelining_api.cpp @@ -90,30 +90,44 @@ static const vx_uint32 WAIT_TIMEOUT_MS = 10000; // helpers // --------------------------------------------------------------------------- +// The images here are not all the same size, so both of these work from the size +// the image reports rather than assuming the default one. +static bool u8_image_size(vx_image img, vx_uint32 & w, vx_uint32 & h) +{ + return vxQueryImage(img, VX_IMAGE_WIDTH, &w, sizeof(w)) == VX_SUCCESS && + vxQueryImage(img, VX_IMAGE_HEIGHT, &h, sizeof(h)) == VX_SUCCESS; +} + static vx_status fill_u8_image(vx_image img, vx_uint8 value) { - std::vector buf((size_t)IMG_W * IMG_H, value); - vx_rectangle_t rect = { 0, 0, IMG_W, IMG_H }; + vx_uint32 w = 0, h = 0; + if (!u8_image_size(img, w, h)) + return VX_FAILURE; + std::vector buf((size_t)w * h, value); + vx_rectangle_t rect = { 0, 0, w, h }; vx_imagepatch_addressing_t addr; memset(&addr, 0, sizeof(addr)); - addr.dim_x = IMG_W; - addr.dim_y = IMG_H; + addr.dim_x = w; + addr.dim_y = h; addr.stride_x = 1; - addr.stride_y = (vx_int32)IMG_W; + addr.stride_y = (vx_int32)w; return vxCopyImagePatch(img, &rect, 0, &addr, buf.data(), VX_WRITE_ONLY, VX_MEMORY_TYPE_HOST); } // Reads the image back and reports whether every pixel holds the expected value. static bool u8_image_is_uniform(vx_image img, vx_uint8 expected) { - std::vector buf((size_t)IMG_W * IMG_H, 0); - vx_rectangle_t rect = { 0, 0, IMG_W, IMG_H }; + vx_uint32 w = 0, h = 0; + if (!u8_image_size(img, w, h)) + return false; + std::vector buf((size_t)w * h, 0); + vx_rectangle_t rect = { 0, 0, w, h }; vx_imagepatch_addressing_t addr; memset(&addr, 0, sizeof(addr)); - addr.dim_x = IMG_W; - addr.dim_y = IMG_H; + addr.dim_x = w; + addr.dim_y = h; addr.stride_x = 1; - addr.stride_y = (vx_int32)IMG_W; + addr.stride_y = (vx_int32)w; if (vxCopyImagePatch(img, &rect, 0, &addr, buf.data(), VX_READ_ONLY, VX_MEMORY_TYPE_HOST) != VX_SUCCESS) return false; for (size_t i = 0; i < buf.size(); i++) { @@ -197,6 +211,75 @@ static void set_event_timeout(vx_context context) vxSetContextAttribute(context, VX_CONTEXT_EVENT_TIMEOUT, &timeout, sizeof(timeout)); } +// A pointer that is not a valid reference. The framework recognises references by +// a magic value, so zeroed storage is rejected, and it is the test's own storage +// so nothing is read that does not belong to it. +struct FakeRef { unsigned char storage[2048]; }; + +static vx_reference invalid_reference(FakeRef & f) +{ + memset(f.storage, 0, sizeof(f.storage)); + return (vx_reference)f.storage; +} + +// Two user kernels with the same shape, one that always fails and one that always +// succeeds. A user node is also the way to get a node that graph optimization +// leaves alone, which the built-in nodes do not. +static const vx_enum FAILING_KERNEL_ID = VX_KERNEL_BASE(VX_ID_USER, 0) + 7; +static const vx_enum PASSTHROUGH_KERNEL_ID = VX_KERNEL_BASE(VX_ID_USER, 0) + 8; + +static vx_status VX_CALLBACK failing_kernel_func(vx_node, const vx_reference *, vx_uint32) +{ + return VX_FAILURE; +} + +static vx_status VX_CALLBACK passthrough_kernel_func(vx_node, const vx_reference *, vx_uint32) +{ + return VX_SUCCESS; +} + +static vx_status VX_CALLBACK failing_kernel_validate(vx_node, const vx_reference parameters[], + vx_uint32 num, vx_meta_format metas[]) +{ + if (num != 2) + return VX_ERROR_INVALID_PARAMETERS; + vx_df_image fmt = VX_DF_IMAGE_U8; + vx_uint32 w = IMG_W, h = IMG_H; + vxSetMetaFormatAttribute(metas[1], VX_IMAGE_FORMAT, &fmt, sizeof(fmt)); + vxSetMetaFormatAttribute(metas[1], VX_IMAGE_WIDTH, &w, sizeof(w)); + vxSetMetaFormatAttribute(metas[1], VX_IMAGE_HEIGHT, &h, sizeof(h)); + (void)parameters; + return VX_SUCCESS; +} + +static vx_kernel register_user_kernel(vx_context context, const char * name, vx_enum id, + vx_kernel_f func) +{ + vx_kernel k = vxAddUserKernel(context, name, id, func, 2, failing_kernel_validate, + nullptr, nullptr); + if (!k) + return nullptr; + if (vxAddParameterToKernel(k, 0, VX_INPUT, VX_TYPE_IMAGE, VX_PARAMETER_STATE_REQUIRED) != VX_SUCCESS || + vxAddParameterToKernel(k, 1, VX_OUTPUT, VX_TYPE_IMAGE, VX_PARAMETER_STATE_REQUIRED) != VX_SUCCESS || + vxFinalizeKernel(k) != VX_SUCCESS) { + vxRemoveKernel(k); + return nullptr; + } + return k; +} + +static vx_kernel register_failing_kernel(vx_context context) +{ + return register_user_kernel(context, "org.mivisionx.test.always_fails", + FAILING_KERNEL_ID, failing_kernel_func); +} + +static vx_kernel register_passthrough_kernel(vx_context context) +{ + return register_user_kernel(context, "org.mivisionx.test.passthrough", + PASSTHROUGH_KERNEL_ID, passthrough_kernel_func); +} + // --------------------------------------------------------------------------- // Test 1: vxSetGraphScheduleConfig argument validation // --------------------------------------------------------------------------- @@ -253,6 +336,13 @@ static int test_schedule_config_validation() EXPECT_STATUS(vxSetGraphScheduleConfig(g.graph, VX_GRAPH_SCHEDULE_MODE_QUEUE_MANUAL, 1, q), VX_ERROR_INVALID_PARAMETERS, "null entry in refs_list rejected"); + // An entry that is not null but is not a reference either. + FakeRef fake; + vx_reference invalid_list[2] = { (vx_reference)g.in, invalid_reference(fake) }; + q[0].refs_list = invalid_list; + EXPECT_STATUS(vxSetGraphScheduleConfig(g.graph, VX_GRAPH_SCHEDULE_MODE_QUEUE_MANUAL, 1, q), + VX_ERROR_INVALID_REFERENCE, "invalid entry in refs_list rejected"); + // [spec] "When this API is called before vxVerifyGraph, the refs_list field // can be NULL, if the reference handles are not available yet at the // application. However refs_list_size MUST always be specified." @@ -1062,6 +1152,865 @@ static int test_concurrent_refs_handover() return errors; } +// --------------------------------------------------------------------------- +// Test 13: what a post-verify vxSetGraphScheduleConfig will and will not accept +// +// After verify the call may only supply refs_list. Everything else has to match +// the configuration the graph was verified with. +// --------------------------------------------------------------------------- +static int test_post_verify_config_validation() +{ + int errors = 0; + printf("\n=== Test 13: post-verify schedule config validation ===\n"); + + vx_context context = vxCreateContext(); + NotGraph g = make_not_graph(context, 2); + + vx_reference in_ref = (vx_reference)g.in; + vx_graph_parameter_queue_params_t q[1]; + memset(q, 0, sizeof(q)); + q[0].graph_parameter_index = 0; + q[0].refs_list_size = 1; + q[0].refs_list = &in_ref; + CHECK_STATUS(vxSetGraphScheduleConfig(g.graph, VX_GRAPH_SCHEDULE_MODE_QUEUE_MANUAL, 1, q)); + EXPECT_STATUS(vxVerifyGraph(g.graph), VX_SUCCESS, "vxVerifyGraph"); + + vx_graph_parameter_queue_params_t p[1]; + + memcpy(p, q, sizeof(p)); + p[0].graph_parameter_index = 99; + EXPECT_STATUS(vxSetGraphScheduleConfig(g.graph, VX_GRAPH_SCHEDULE_MODE_QUEUE_MANUAL, 1, p), + VX_ERROR_INVALID_PARAMETERS, "out of range parameter index rejected after verify"); + + memcpy(p, q, sizeof(p)); + p[0].refs_list_size = 0; + EXPECT_STATUS(vxSetGraphScheduleConfig(g.graph, VX_GRAPH_SCHEDULE_MODE_QUEUE_MANUAL, 1, p), + VX_ERROR_INVALID_PARAMETERS, "zero refs_list_size rejected after verify"); + + // Parameter 1 was never given a queue, and this call may not create one. + memcpy(p, q, sizeof(p)); + p[0].graph_parameter_index = 1; + EXPECT_STATUS(vxSetGraphScheduleConfig(g.graph, VX_GRAPH_SCHEDULE_MODE_QUEUE_MANUAL, 1, p), + VX_ERROR_INVALID_PARAMETERS, "a queue cannot be enabled after verify"); + + // A null refs_list is not an instruction to forget the references already + // supplied, so the previous list survives. + memcpy(p, q, sizeof(p)); + p[0].refs_list = nullptr; + EXPECT_STATUS(vxSetGraphScheduleConfig(g.graph, VX_GRAPH_SCHEDULE_MODE_QUEUE_MANUAL, 1, p), + VX_SUCCESS, "null refs_list after verify accepted"); + { + vx_reference got[1] = { nullptr }; + EXPECT_STATUS(vxGetGraphParameterRefsList(g.graph, 0, 1, got), VX_SUCCESS, + "refs list still readable"); + EXPECT_TRUE(got[0] == in_ref, "the previously supplied list is retained"); + } + + { + vx_reference nulls[1] = { nullptr }; + memcpy(p, q, sizeof(p)); + p[0].refs_list = nulls; + EXPECT_STATUS(vxSetGraphScheduleConfig(g.graph, VX_GRAPH_SCHEDULE_MODE_QUEUE_MANUAL, 1, p), + VX_ERROR_INVALID_PARAMETERS, "null entry rejected after verify"); + + FakeRef fake; + vx_reference invalid[1] = { invalid_reference(fake) }; + memcpy(p, q, sizeof(p)); + p[0].refs_list = invalid; + EXPECT_STATUS(vxSetGraphScheduleConfig(g.graph, VX_GRAPH_SCHEDULE_MODE_QUEUE_MANUAL, 1, p), + VX_ERROR_INVALID_REFERENCE, "invalid entry rejected after verify"); + } + + // A rejected list must not have disturbed the one already in place. + { + vx_reference got[1] = { nullptr }; + EXPECT_STATUS(vxGetGraphParameterRefsList(g.graph, 0, 1, got), VX_SUCCESS, + "refs list readable after a rejected call"); + EXPECT_TRUE(got[0] == in_ref, "a rejected call leaves the list unchanged"); + } + + release_not_graph(g); + vxReleaseContext(&context); + return errors; +} + +// --------------------------------------------------------------------------- +// Test 14: vxAddReferencesToGraphParameterList argument validation +// --------------------------------------------------------------------------- +static int test_add_references_validation() +{ + int errors = 0; + printf("\n=== Test 14: vxAddReferencesToGraphParameterList validation ===\n"); + + vx_context context = vxCreateContext(); + NotGraph g = make_not_graph(context, 2); + + vx_image spare = vxCreateImage(context, IMG_W, IMG_H, VX_DF_IMAGE_U8); + vx_reference refs[2] = { (vx_reference)g.in, (vx_reference)spare }; + vx_graph_parameter_queue_params_t q[1]; + memset(q, 0, sizeof(q)); + q[0].graph_parameter_index = 0; + q[0].refs_list_size = 2; + q[0].refs_list = refs; + CHECK_STATUS(vxSetGraphScheduleConfig(g.graph, VX_GRAPH_SCHEDULE_MODE_QUEUE_MANUAL, 1, q)); + EXPECT_STATUS(vxVerifyGraph(g.graph), VX_SUCCESS, "vxVerifyGraph"); + + vx_reference add = (vx_reference)g.in; + EXPECT_STATUS(vxAddReferencesToGraphParameterList(nullptr, 0, 1, &add), + VX_ERROR_INVALID_GRAPH, "null graph rejected"); + EXPECT_STATUS(vxAddReferencesToGraphParameterList(g.graph, 0, 0, &add), + VX_ERROR_INVALID_PARAMETERS, "adding nothing rejected"); + EXPECT_STATUS(vxAddReferencesToGraphParameterList(g.graph, 0, 1, nullptr), + VX_ERROR_INVALID_PARAMETERS, "null reference array rejected"); + EXPECT_STATUS(vxAddReferencesToGraphParameterList(g.graph, 99, 1, &add), + VX_ERROR_INVALID_PARAMETERS, "out of range parameter rejected"); + { + vx_reference nulls[1] = { nullptr }; + EXPECT_STATUS(vxAddReferencesToGraphParameterList(g.graph, 0, 1, nulls), + VX_ERROR_INVALID_REFERENCE, "null entry rejected"); + FakeRef fake; + vx_reference invalid[1] = { invalid_reference(fake) }; + EXPECT_STATUS(vxAddReferencesToGraphParameterList(g.graph, 0, 1, invalid), + VX_ERROR_INVALID_REFERENCE, "invalid entry rejected"); + } + + vxReleaseImage(&spare); + release_not_graph(g); + vxReleaseContext(&context); + return errors; +} + +// --------------------------------------------------------------------------- +// Test 15: graph parameters that were never given a queue +// +// Only some of a graph's parameters need to be queued. The ones that were not +// still have to behave sensibly, and an execution has to skip over them. +// --------------------------------------------------------------------------- +static int test_unqueued_parameters() +{ + int errors = 0; + printf("\n=== Test 15: parameters without a queue ===\n"); + + vx_context context = vxCreateContext(); + NotGraph g = make_not_graph(context, 2); + CHECK_STATUS(fill_u8_image(g.in, 0x22)); + + // Only the output, parameter 1, is queued. + vx_reference out_ref = (vx_reference)g.out; + vx_graph_parameter_queue_params_t q[1]; + memset(q, 0, sizeof(q)); + q[0].graph_parameter_index = 1; + q[0].refs_list_size = 1; + q[0].refs_list = &out_ref; + CHECK_STATUS(vxSetGraphScheduleConfig(g.graph, VX_GRAPH_SCHEDULE_MODE_QUEUE_MANUAL, 1, q)); + EXPECT_STATUS(vxVerifyGraph(g.graph), VX_SUCCESS, "vxVerifyGraph"); + + // Parameter 0 is an input and has no queue, so it cannot be enqueued to. + { + vx_reference in_ref = (vx_reference)g.in; + EXPECT_STATUS(vxGraphParameterEnqueueReadyRef(g.graph, 0, &in_ref, 1), + VX_ERROR_INVALID_PARAMETERS, "enqueue on an unqueued input rejected"); + } + // Nor dequeued from. + { + vx_reference deq = nullptr; + vx_uint32 num = 0; + EXPECT_STATUS(vxGraphParameterDequeueDoneRef(g.graph, 0, &deq, 1, &num), + VX_ERROR_INVALID_PARAMETERS, "dequeue on an unqueued parameter rejected"); + } + + // The execution consumes from the queued parameter and leaves the other bound + // to the reference it was created with. + EXPECT_STATUS(vxGraphParameterEnqueueReadyRef(g.graph, 1, &out_ref, 1), + VX_SUCCESS, "output enqueued"); + EXPECT_STATUS(vxProcessGraph(g.graph), VX_SUCCESS, "graph runs with one queue configured"); + EXPECT_TRUE(u8_image_is_uniform(g.out, (vx_uint8)~0x22), + "the unqueued input was still used as the graph input"); + { + vx_reference deq = nullptr; + vx_uint32 num = 0; + EXPECT_STATUS(vxGraphParameterDequeueDoneRef(g.graph, 1, &deq, 1, &num), + VX_SUCCESS, "output dequeued"); + EXPECT_TRUE(num == 1 && deq == out_ref, "the dequeued reference is the one enqueued"); + } + + release_not_graph(g); + vxReleaseContext(&context); + return errors; +} + +// --------------------------------------------------------------------------- +// Test 16: the timeouts actually expire +// +// VX_GRAPH_TIMEOUT and VX_CONTEXT_EVENT_TIMEOUT are only useful if a wait with +// nothing to wait for gives up instead of blocking forever. +// --------------------------------------------------------------------------- +static int test_timeouts_expire() +{ + int errors = 0; + printf("\n=== Test 16: waits give up when the timeout expires ===\n"); + + const vx_uint32 SHORT_MS = 50; + + vx_context context = vxCreateContext(); + NotGraph g = make_not_graph(context, 2); + + vx_reference in_ref = (vx_reference)g.in; + vx_reference out_ref = (vx_reference)g.out; + vx_graph_parameter_queue_params_t q[2]; + memset(q, 0, sizeof(q)); + q[0].graph_parameter_index = 0; + q[0].refs_list_size = 1; + q[0].refs_list = &in_ref; + q[1].graph_parameter_index = 1; + q[1].refs_list_size = 1; + q[1].refs_list = &out_ref; + CHECK_STATUS(vxSetGraphScheduleConfig(g.graph, VX_GRAPH_SCHEDULE_MODE_QUEUE_MANUAL, 2, q)); + CHECK_STATUS(vxSetGraphAttribute(g.graph, VX_GRAPH_TIMEOUT, &SHORT_MS, sizeof(SHORT_MS))); + EXPECT_STATUS(vxVerifyGraph(g.graph), VX_SUCCESS, "vxVerifyGraph"); + + // Nothing has been executed, so there is no done reference to collect and the + // blocking form has to return once the graph timeout has passed. + { + vx_reference deq = nullptr; + vx_uint32 num = 0; + vx_status s = vxGraphParameterDequeueDoneRef(g.graph, 1, &deq, 1, &num); + EXPECT_TRUE(s != VX_SUCCESS, "a blocking dequeue gives up once VX_GRAPH_TIMEOUT passes"); + } + + // Same for the event queue, which has its own timeout. + { + vx_uint32 t = SHORT_MS; + CHECK_STATUS(vxSetContextAttribute(context, VX_CONTEXT_EVENT_TIMEOUT, &t, sizeof(t))); + CHECK_STATUS(vxEnableEvents(context)); + vx_event_t ev; + memset(&ev, 0, sizeof(ev)); + vx_status s = vxWaitEvent(context, &ev, vx_false_e); + EXPECT_TRUE(s != VX_SUCCESS, "a blocking event wait gives up once the timeout passes"); + } + + release_not_graph(g); + vxReleaseContext(&context); + return errors; +} + +// --------------------------------------------------------------------------- +// Test 17: a node that fails +// +// A failing execution has to report VX_EVENT_NODE_ERROR for a node the +// application registered, carry the node's status in the event, and still report +// the graph as completed. +// --------------------------------------------------------------------------- +static int test_node_error_events() +{ + int errors = 0; + printf("\n=== Test 17: node error reporting ===\n"); + + vx_context context = vxCreateContext(); + set_event_timeout(context); + + vx_kernel kernel = register_failing_kernel(context); + CHECK_NOT_NULL(kernel, "vxAddUserKernel"); + if (!kernel) { + vxReleaseContext(&context); + return errors; + } + + vx_graph graph = vxCreateGraph(context); + vx_image in = vxCreateImage(context, IMG_W, IMG_H, VX_DF_IMAGE_U8); + vx_image out = vxCreateImage(context, IMG_W, IMG_H, VX_DF_IMAGE_U8); + vx_node node = vxCreateGenericNode(graph, kernel); + CHECK_NOT_NULL(node, "vxCreateGenericNode"); + CHECK_STATUS(vxSetParameterByIndex(node, 0, (vx_reference)in)); + CHECK_STATUS(vxSetParameterByIndex(node, 1, (vx_reference)out)); + CHECK_STATUS(fill_u8_image(in, 0x33)); + + for (vx_uint32 i = 0; i < 2; i++) { + vx_parameter prm = vxGetParameterByIndex(node, i); + if (prm) { + vxAddParameterToGraph(graph, prm); + vxReleaseParameter(&prm); + } + } + + // Registering for both node events on the same node proves the framework + // picks the matching registration rather than the first one it finds. + CHECK_STATUS(vxRegisterEvent((vx_reference)node, VX_EVENT_NODE_ERROR, 0, 1701)); + CHECK_STATUS(vxRegisterEvent((vx_reference)node, VX_EVENT_NODE_COMPLETED, 0, 1702)); + CHECK_STATUS(vxRegisterEvent((vx_reference)graph, VX_EVENT_GRAPH_COMPLETED, 0, 1703)); + + vx_reference in_ref = (vx_reference)in; + vx_reference out_ref = (vx_reference)out; + vx_graph_parameter_queue_params_t q[2]; + memset(q, 0, sizeof(q)); + q[0].graph_parameter_index = 0; + q[0].refs_list_size = 1; + q[0].refs_list = &in_ref; + q[1].graph_parameter_index = 1; + q[1].refs_list_size = 1; + q[1].refs_list = &out_ref; + CHECK_STATUS(vxSetGraphScheduleConfig(graph, VX_GRAPH_SCHEDULE_MODE_QUEUE_MANUAL, 2, q)); + { + vx_uint32 timeout = WAIT_TIMEOUT_MS; + CHECK_STATUS(vxSetGraphAttribute(graph, VX_GRAPH_TIMEOUT, &timeout, sizeof(timeout))); + } + CHECK_STATUS(vxEnableEvents(context)); + EXPECT_STATUS(vxVerifyGraph(graph), VX_SUCCESS, "vxVerifyGraph with a user node"); + + CHECK_STATUS(vxGraphParameterEnqueueReadyRef(graph, 0, &in_ref, 1)); + CHECK_STATUS(vxGraphParameterEnqueueReadyRef(graph, 1, &out_ref, 1)); + + // The kernel always fails, so the execution has to fail too. + EXPECT_TRUE(vxProcessGraph(graph) != VX_SUCCESS, "a failing kernel fails the execution"); + + EventTally t = drain_events(context); + EXPECT_TRUE(t.node_error == 1, "exactly one VX_EVENT_NODE_ERROR reported"); + EXPECT_TRUE(t.node_completed == 0, "no node completion reported for a node that failed"); + EXPECT_TRUE(t.graph_completed == 1, "graph completion still reported for a failed execution"); + bool saw_error_app_value = false; + for (vx_uint32 v : t.app_values) { + if (v == 1701) saw_error_app_value = true; + } + EXPECT_TRUE(saw_error_app_value, "the node error carries the app_value registered for it"); + + vxReleaseNode(&node); + vxReleaseImage(&in); + vxReleaseImage(&out); + vxReleaseGraph(&graph); + vxRemoveKernel(kernel); + vxReleaseContext(&context); + return errors; +} + +// --------------------------------------------------------------------------- +// Test 18: VX_REFERENCE_ENQUEUE_COUNT through a whole cycle +// +// The attribute exists so an application can tell whether a reference is still +// owned by a queue and therefore unsafe to touch. +// --------------------------------------------------------------------------- +static int test_enqueue_count_lifecycle() +{ + int errors = 0; + printf("\n=== Test 18: VX_REFERENCE_ENQUEUE_COUNT lifecycle ===\n"); + + vx_context context = vxCreateContext(); + NotGraph g = make_not_graph(context, 2); + CHECK_STATUS(fill_u8_image(g.in, 0x44)); + + vx_reference in_ref = (vx_reference)g.in; + vx_reference out_ref = (vx_reference)g.out; + vx_graph_parameter_queue_params_t q[2]; + memset(q, 0, sizeof(q)); + q[0].graph_parameter_index = 0; + q[0].refs_list_size = 1; + q[0].refs_list = &in_ref; + q[1].graph_parameter_index = 1; + q[1].refs_list_size = 1; + q[1].refs_list = &out_ref; + CHECK_STATUS(vxSetGraphScheduleConfig(g.graph, VX_GRAPH_SCHEDULE_MODE_QUEUE_MANUAL, 2, q)); + { + vx_uint32 timeout = WAIT_TIMEOUT_MS; + CHECK_STATUS(vxSetGraphAttribute(g.graph, VX_GRAPH_TIMEOUT, &timeout, sizeof(timeout))); + } + EXPECT_STATUS(vxVerifyGraph(g.graph), VX_SUCCESS, "vxVerifyGraph"); + + auto count_of = [&](vx_reference r) -> vx_uint32 { + vx_uint32 c = 0xFFFFFFFF; + if (vxQueryReference(r, VX_REFERENCE_ENQUEUE_COUNT, &c, sizeof(c)) != VX_SUCCESS) + return 0xFFFFFFFF; + return c; + }; + + EXPECT_TRUE(count_of(in_ref) == 0, "a reference that was never enqueued counts zero"); + + CHECK_STATUS(vxGraphParameterEnqueueReadyRef(g.graph, 0, &in_ref, 1)); + EXPECT_TRUE(count_of(in_ref) == 1, "a reference waiting in the ready queue counts one"); + + CHECK_STATUS(vxGraphParameterEnqueueReadyRef(g.graph, 1, &out_ref, 1)); + EXPECT_STATUS(vxProcessGraph(g.graph), VX_SUCCESS, "graph runs"); + + // The execution is over but the application has not collected the references + // yet, so they are still owned by the queues. + EXPECT_TRUE(count_of(out_ref) == 1, "a reference waiting to be dequeued still counts"); + + { + vx_reference deq = nullptr; + vx_uint32 num = 0; + CHECK_STATUS(vxGraphParameterDequeueDoneRef(g.graph, 0, &deq, 1, &num)); + CHECK_STATUS(vxGraphParameterDequeueDoneRef(g.graph, 1, &deq, 1, &num)); + } + EXPECT_TRUE(count_of(in_ref) == 0, "the count drops back to zero once collected"); + EXPECT_TRUE(count_of(out_ref) == 0, "the same for the output"); + + // A reference that belongs to no graph at all is also countable. + { + vx_image loose = vxCreateImage(context, IMG_W, IMG_H, VX_DF_IMAGE_U8); + EXPECT_TRUE(count_of((vx_reference)loose) == 0, "a reference outside any queue counts zero"); + vxReleaseImage(&loose); + } + + // The count is per context, so a second graph in the same context that does + // no queueing at all has to be walked over rather than tripped on. + { + NotGraph plain = make_not_graph(context, 0); + CHECK_STATUS(vxVerifyGraph(plain.graph)); + CHECK_STATUS(vxGraphParameterEnqueueReadyRef(g.graph, 0, &in_ref, 1)); + EXPECT_TRUE(count_of(in_ref) == 1, + "an unpipelined graph in the same context does not disturb the count"); + { + vx_reference deq = nullptr; + vx_uint32 num = 0; + CHECK_STATUS(vxGraphParameterEnqueueReadyRef(g.graph, 1, &out_ref, 1)); + CHECK_STATUS(vxProcessGraph(g.graph)); + CHECK_STATUS(vxGraphParameterDequeueDoneRef(g.graph, 0, &deq, 1, &num)); + CHECK_STATUS(vxGraphParameterDequeueDoneRef(g.graph, 1, &deq, 1, &num)); + } + release_not_graph(plain); + } + + release_not_graph(g); + vxReleaseContext(&context); + return errors; +} + +// --------------------------------------------------------------------------- +// Test 19: scheduling a QUEUE_AUTO graph explicitly, and tearing down a graph +// that is still streaming +// --------------------------------------------------------------------------- +static int test_auto_schedule_and_teardown() +{ + int errors = 0; + printf("\n=== Test 19: explicit schedule under QUEUE_AUTO, teardown while streaming ===\n"); + + // In QUEUE_AUTO the framework schedules by itself, so an explicit request has + // nothing to add and must not be treated as an error. + { + vx_context context = vxCreateContext(); + NotGraph g = make_not_graph(context, 2); + CHECK_STATUS(fill_u8_image(g.in, 0x55)); + + vx_reference in_ref = (vx_reference)g.in; + vx_reference out_ref = (vx_reference)g.out; + vx_graph_parameter_queue_params_t q[2]; + memset(q, 0, sizeof(q)); + q[0].graph_parameter_index = 0; + q[0].refs_list_size = 1; + q[0].refs_list = &in_ref; + q[1].graph_parameter_index = 1; + q[1].refs_list_size = 1; + q[1].refs_list = &out_ref; + CHECK_STATUS(vxSetGraphScheduleConfig(g.graph, VX_GRAPH_SCHEDULE_MODE_QUEUE_AUTO, 2, q)); + { + vx_uint32 timeout = WAIT_TIMEOUT_MS; + CHECK_STATUS(vxSetGraphAttribute(g.graph, VX_GRAPH_TIMEOUT, &timeout, sizeof(timeout))); + } + EXPECT_STATUS(vxVerifyGraph(g.graph), VX_SUCCESS, "vxVerifyGraph"); + EXPECT_STATUS(vxScheduleGraph(g.graph), VX_SUCCESS, + "an explicit schedule under QUEUE_AUTO is accepted"); + EXPECT_STATUS(vxWaitGraph(g.graph), VX_SUCCESS, "waiting on it is accepted"); + + CHECK_STATUS(vxGraphParameterEnqueueReadyRef(g.graph, 0, &in_ref, 1)); + CHECK_STATUS(vxGraphParameterEnqueueReadyRef(g.graph, 1, &out_ref, 1)); + { + vx_reference deq = nullptr; + vx_uint32 num = 0; + EXPECT_STATUS(vxGraphParameterDequeueDoneRef(g.graph, 1, &deq, 1, &num), + VX_SUCCESS, "the executor still runs the graph on its own"); + } + release_not_graph(g); + vxReleaseContext(&context); + } + + // Releasing a graph without stopping streaming first has to shut the thread + // down rather than leave it running against a freed graph. + { + vx_context context = vxCreateContext(); + NotGraph g = make_not_graph(context, 0); + CHECK_STATUS(fill_u8_image(g.in, 0x66)); + EXPECT_STATUS(vxEnableGraphStreaming(g.graph, g.node), VX_SUCCESS, "streaming enabled"); + EXPECT_STATUS(vxVerifyGraph(g.graph), VX_SUCCESS, "vxVerifyGraph"); + EXPECT_STATUS(vxStartGraphStreaming(g.graph), VX_SUCCESS, "streaming started"); + // Long enough for the streaming thread to get through several iterations. + std::this_thread::sleep_for(std::chrono::milliseconds(50)); + release_not_graph(g); + printf(" PASS: graph released while streaming was still running\n"); + vxReleaseContext(&context); + } + + return errors; +} + +// --------------------------------------------------------------------------- +// Test 20: the kernel-side surface of the extension +// --------------------------------------------------------------------------- +static int test_kernel_surface() +{ + int errors = 0; + printf("\n=== Test 20: kernel attributes and parameter config ===\n"); + + vx_context context = vxCreateContext(); + NotGraph g = make_not_graph(context, 0); + + vx_kernel kernel = vxGetKernelByEnum(context, VX_KERNEL_NOT); + CHECK_NOT_NULL(kernel, "vxGetKernelByEnum"); + + // vxGetKernelParameterConfig is part of the extension's header but is not + // implemented here, and the spec's answer for that is NOT_SUPPORTED rather + // than a crash or a false success. + { + vx_kernel_parameter_config_t cfg[2]; + memset(cfg, 0, sizeof(cfg)); + vx_status s = vxGetKernelParameterConfig(kernel, 2, cfg); + EXPECT_TRUE(s == VX_ERROR_NOT_SUPPORTED || s == VX_SUCCESS, + "vxGetKernelParameterConfig reports a defined status"); + printf(" INFO: vxGetKernelParameterConfig returned %d\n", s); + } + + // The pipeup depths are kernel attributes a user kernel would set. A kernel + // that has already been finalized cannot have them changed. + { + vx_uint32 depth = 0; + vx_status s = vxQueryKernel(kernel, VX_KERNEL_PIPEUP_OUTPUT_DEPTH, &depth, sizeof(depth)); + if (s == VX_SUCCESS) { + EXPECT_TRUE(depth >= 1, "VX_KERNEL_PIPEUP_OUTPUT_DEPTH is at least one"); + s = vxQueryKernel(kernel, VX_KERNEL_PIPEUP_INPUT_DEPTH, &depth, sizeof(depth)); + EXPECT_STATUS(s, VX_SUCCESS, "VX_KERNEL_PIPEUP_INPUT_DEPTH queryable"); + vx_uint32 bad = 0; + EXPECT_STATUS(vxSetKernelAttribute(kernel, VX_KERNEL_PIPEUP_OUTPUT_DEPTH, &bad, sizeof(bad)), + VX_ERROR_INVALID_PARAMETERS, "a depth below one is rejected"); + vx_uint32 two = 2; + EXPECT_STATUS(vxSetKernelAttribute(kernel, VX_KERNEL_PIPEUP_OUTPUT_DEPTH, &two, sizeof(two)), + VX_ERROR_INVALID_PARAMETERS, "a finalized kernel rejects the change"); + } else { + printf(" INFO: VX_KERNEL_PIPEUP_OUTPUT_DEPTH returned %d\n", s); + } + } + + // VX_NODE_STATE belongs to this extension too. + { + vx_uint32 state = 0xFFFFFFFF; + EXPECT_STATUS(vxQueryNode(g.node, VX_NODE_STATE, &state, sizeof(state)), + VX_SUCCESS, "VX_NODE_STATE queryable"); + EXPECT_TRUE(state == VX_NODE_STATE_STEADY || state == VX_NODE_STATE_PIPEUP, + "VX_NODE_STATE reports a defined state"); + } + + vxReleaseKernel(&kernel); + release_not_graph(g); + vxReleaseContext(&context); + return errors; +} + +// --------------------------------------------------------------------------- +// Test 21: the graph-scoped event calls reject a null graph +// --------------------------------------------------------------------------- +static int test_graph_event_null_handling() +{ + int errors = 0; + printf("\n=== Test 21: graph event calls with a null graph ===\n"); + + vx_event_t ev; + memset(&ev, 0, sizeof(ev)); + EXPECT_STATUS(vxWaitGraphEvent(nullptr, &ev, vx_true_e), + VX_ERROR_INVALID_REFERENCE, "vxWaitGraphEvent rejects a null graph"); + EXPECT_STATUS(vxEnableGraphEvents(nullptr), + VX_ERROR_INVALID_REFERENCE, "vxEnableGraphEvents rejects a null graph"); + EXPECT_STATUS(vxDisableGraphEvents(nullptr), + VX_ERROR_INVALID_REFERENCE, "vxDisableGraphEvents rejects a null graph"); + EXPECT_STATUS(vxSendUserGraphEvent(nullptr, 1, nullptr), + VX_ERROR_INVALID_REFERENCE, "vxSendUserGraphEvent rejects a null graph"); + EXPECT_STATUS(vxRegisterGraphEvent(nullptr, VX_EVENT_GRAPH_COMPLETED, 0, 1), + VX_ERROR_INVALID_REFERENCE, "vxRegisterGraphEvent rejects a null reference"); + EXPECT_STATUS(vxEnableGraphStreaming(nullptr, nullptr), + VX_ERROR_INVALID_REFERENCE, "vxEnableGraphStreaming rejects a null graph"); + EXPECT_STATUS(vxStartGraphStreaming(nullptr), + VX_ERROR_INVALID_REFERENCE, "vxStartGraphStreaming rejects a null graph"); + EXPECT_STATUS(vxStopGraphStreaming(nullptr), + VX_ERROR_INVALID_REFERENCE, "vxStopGraphStreaming rejects a null graph"); + return errors; +} + +// --------------------------------------------------------------------------- +// Test 22: node completion events from a pipelined execution +// +// Registrations are held per context, so an execution has to report the nodes of +// the graph that ran and leave every other registration alone. +// --------------------------------------------------------------------------- +static int test_node_events_under_pipelining() +{ + int errors = 0; + printf("\n=== Test 22: node events from a pipelined execution ===\n"); + + vx_context context = vxCreateContext(); + set_event_timeout(context); + + NotGraph a = make_not_graph(context, 2); + NotGraph b = make_not_graph(context, 0); + CHECK_STATUS(fill_u8_image(a.in, 0x77)); + + // One registration for a node of the graph that will run, one for a node of a + // graph that will not. + CHECK_STATUS(vxRegisterEvent((vx_reference)a.node, VX_EVENT_NODE_COMPLETED, 0, 2201)); + CHECK_STATUS(vxRegisterEvent((vx_reference)b.node, VX_EVENT_NODE_COMPLETED, 0, 2202)); + CHECK_STATUS(vxRegisterEvent((vx_reference)a.graph, VX_EVENT_GRAPH_COMPLETED, 0, 2203)); + + vx_reference in_ref = (vx_reference)a.in; + vx_reference out_ref = (vx_reference)a.out; + vx_graph_parameter_queue_params_t q[2]; + memset(q, 0, sizeof(q)); + q[0].graph_parameter_index = 0; + q[0].refs_list_size = 1; + q[0].refs_list = &in_ref; + q[1].graph_parameter_index = 1; + q[1].refs_list_size = 1; + q[1].refs_list = &out_ref; + CHECK_STATUS(vxSetGraphScheduleConfig(a.graph, VX_GRAPH_SCHEDULE_MODE_QUEUE_MANUAL, 2, q)); + { + vx_uint32 timeout = WAIT_TIMEOUT_MS; + CHECK_STATUS(vxSetGraphAttribute(a.graph, VX_GRAPH_TIMEOUT, &timeout, sizeof(timeout))); + } + CHECK_STATUS(vxEnableEvents(context)); + CHECK_STATUS(vxVerifyGraph(a.graph)); + CHECK_STATUS(vxGraphParameterEnqueueReadyRef(a.graph, 0, &in_ref, 1)); + CHECK_STATUS(vxGraphParameterEnqueueReadyRef(a.graph, 1, &out_ref, 1)); + EXPECT_STATUS(vxProcessGraph(a.graph), VX_SUCCESS, "pipelined execution"); + + EventTally t = drain_events(context); + EXPECT_TRUE(t.node_completed >= 1, "the node of the graph that ran reported completion"); + bool saw_other_graph = false, saw_this_graph = false; + for (vx_uint32 v : t.app_values) { + if (v == 2202) saw_other_graph = true; + if (v == 2201) saw_this_graph = true; + } + EXPECT_TRUE(saw_this_graph, "the completion carries the app_value registered for that node"); + EXPECT_TRUE(!saw_other_graph, "a node of another graph reports nothing"); + EXPECT_TRUE(t.graph_completed == 1, "the graph reported completion once"); + + // Each node reports once per execution and not more. + EXPECT_TRUE(t.node_completed == 1, "the node reported completion exactly once"); + + release_not_graph(a); + release_not_graph(b); + vxReleaseContext(&context); + return errors; +} + +// --------------------------------------------------------------------------- +// Test 24: a user node inside a pipelined graph +// +// A user node is not rewritten by graph optimization, so this is the case where +// the node the application registered against is the node that actually runs. +// --------------------------------------------------------------------------- +static int test_user_node_under_pipelining() +{ + int errors = 0; + printf("\n=== Test 24: a user node in a pipelined graph ===\n"); + + vx_context context = vxCreateContext(); + set_event_timeout(context); + + vx_kernel kernel = register_passthrough_kernel(context); + CHECK_NOT_NULL(kernel, "vxAddUserKernel"); + if (!kernel) { + vxReleaseContext(&context); + return errors; + } + + vx_graph graph = vxCreateGraph(context); + vx_image in = vxCreateImage(context, IMG_W, IMG_H, VX_DF_IMAGE_U8); + vx_image out = vxCreateImage(context, IMG_W, IMG_H, VX_DF_IMAGE_U8); + vx_node node = vxCreateGenericNode(graph, kernel); + CHECK_NOT_NULL(node, "vxCreateGenericNode"); + CHECK_STATUS(vxSetParameterByIndex(node, 0, (vx_reference)in)); + CHECK_STATUS(vxSetParameterByIndex(node, 1, (vx_reference)out)); + CHECK_STATUS(fill_u8_image(in, 0x99)); + for (vx_uint32 i = 0; i < 2; i++) { + vx_parameter prm = vxGetParameterByIndex(node, i); + if (prm) { + vxAddParameterToGraph(graph, prm); + vxReleaseParameter(&prm); + } + } + + CHECK_STATUS(vxRegisterEvent((vx_reference)node, VX_EVENT_NODE_COMPLETED, 0, 2401)); + CHECK_STATUS(vxRegisterEvent((vx_reference)graph, VX_EVENT_GRAPH_COMPLETED, 0, 2402)); + + vx_reference in_ref = (vx_reference)in; + vx_reference out_ref = (vx_reference)out; + vx_graph_parameter_queue_params_t q[2]; + memset(q, 0, sizeof(q)); + q[0].graph_parameter_index = 0; + q[0].refs_list_size = 1; + q[0].refs_list = &in_ref; + q[1].graph_parameter_index = 1; + q[1].refs_list_size = 1; + q[1].refs_list = &out_ref; + CHECK_STATUS(vxSetGraphScheduleConfig(graph, VX_GRAPH_SCHEDULE_MODE_QUEUE_MANUAL, 2, q)); + { + vx_uint32 timeout = WAIT_TIMEOUT_MS; + CHECK_STATUS(vxSetGraphAttribute(graph, VX_GRAPH_TIMEOUT, &timeout, sizeof(timeout))); + } + CHECK_STATUS(vxEnableEvents(context)); + EXPECT_STATUS(vxVerifyGraph(graph), VX_SUCCESS, "vxVerifyGraph"); + CHECK_STATUS(vxGraphParameterEnqueueReadyRef(graph, 0, &in_ref, 1)); + CHECK_STATUS(vxGraphParameterEnqueueReadyRef(graph, 1, &out_ref, 1)); + EXPECT_STATUS(vxProcessGraph(graph), VX_SUCCESS, "pipelined execution of a user node"); + + EventTally t = drain_events(context); + EXPECT_TRUE(t.node_completed == 1, "the user node reported completion exactly once"); + EXPECT_TRUE(t.graph_completed == 1, "the graph reported completion once"); + + { + vx_reference deq = nullptr; + vx_uint32 num = 0; + EXPECT_STATUS(vxGraphParameterDequeueDoneRef(graph, 1, &deq, 1, &num), + VX_SUCCESS, "the output comes back"); + EXPECT_TRUE(num == 1 && deq == out_ref, "and it is the reference that was enqueued"); + } + + vxReleaseNode(&node); + vxReleaseImage(&in); + vxReleaseImage(&out); + vxReleaseGraph(&graph); + vxRemoveKernel(kernel); + vxReleaseContext(&context); + return errors; +} + +// --------------------------------------------------------------------------- +// Test 25: enqueueing an image that another node views through an ROI +// +// Queueing substitutes the reference bound to a graph parameter. An ROI the +// application created over some other image is a reference of its own, and it +// keeps viewing the image it was created from. This pins that down, because the +// alternative -- repointing an application's ROI at whatever was last enqueued -- +// would change the meaning of an object the application still holds. +// --------------------------------------------------------------------------- +static int test_roi_of_queued_image() +{ + int errors = 0; + printf("\n=== Test 25: a queued image with an ROI over it ===\n"); + + const vx_uint8 FILL_A = 0x11; + const vx_uint8 FILL_B = 0x22; + + vx_context context = vxCreateContext(); + vx_graph graph = vxCreateGraph(context); + + vx_image master = vxCreateImage(context, IMG_W, IMG_H, VX_DF_IMAGE_U8); + vx_image master2 = vxCreateImage(context, IMG_W, IMG_H, VX_DF_IMAGE_U8); + vx_rectangle_t rect = { 0, 0, IMG_W / 2, IMG_H / 2 }; + vx_image roi = vxCreateImageFromROI(master, &rect); + CHECK_NOT_NULL(roi, "vxCreateImageFromROI"); + + vx_image out_full = vxCreateImage(context, IMG_W, IMG_H, VX_DF_IMAGE_U8); + vx_image out_roi = vxCreateImage(context, IMG_W / 2, IMG_H / 2, VX_DF_IMAGE_U8); + + vx_node n_full = vxNotNode(graph, master, out_full); + vx_node n_roi = vxNotNode(graph, roi, out_roi); + CHECK_NOT_NULL(n_full, "vxNotNode on the master"); + CHECK_NOT_NULL(n_roi, "vxNotNode on the ROI"); + + // Different content in each image, so which one each node read is visible in + // its output. + CHECK_STATUS(fill_u8_image(master, FILL_A)); + CHECK_STATUS(fill_u8_image(master2, FILL_B)); + + // Graph parameter 0 is the master image. + { + vx_parameter prm = vxGetParameterByIndex(n_full, 0); + CHECK_STATUS(vxAddParameterToGraph(graph, prm)); + vxReleaseParameter(&prm); + } + + vx_reference refs[2] = { (vx_reference)master, (vx_reference)master2 }; + vx_graph_parameter_queue_params_t q[1]; + memset(q, 0, sizeof(q)); + q[0].graph_parameter_index = 0; + q[0].refs_list_size = 2; + q[0].refs_list = refs; + CHECK_STATUS(vxSetGraphScheduleConfig(graph, VX_GRAPH_SCHEDULE_MODE_QUEUE_MANUAL, 1, q)); + { + vx_uint32 timeout = WAIT_TIMEOUT_MS; + CHECK_STATUS(vxSetGraphAttribute(graph, VX_GRAPH_TIMEOUT, &timeout, sizeof(timeout))); + } + EXPECT_STATUS(vxVerifyGraph(graph), VX_SUCCESS, "vxVerifyGraph"); + + // Run once with the image the graph was built with, then once with the other. + for (int pass = 0; pass < 2; pass++) { + vx_reference enq = refs[pass]; + vx_uint8 enqueued_fill = (pass == 0) ? FILL_A : FILL_B; + EXPECT_STATUS(vxGraphParameterEnqueueReadyRef(graph, 0, &enq, 1), VX_SUCCESS, + pass == 0 ? "enqueue the original image" : "enqueue the other image"); + EXPECT_STATUS(vxProcessGraph(graph), VX_SUCCESS, + pass == 0 ? "first execution" : "execution after the swap"); + // The graph parameter follows what was enqueued. + EXPECT_TRUE(u8_image_is_uniform(out_full, (vx_uint8)~enqueued_fill), + "the node on the graph parameter read the enqueued reference"); + // The ROI does not: it still reads the image it was created over. + EXPECT_TRUE(u8_image_is_uniform(out_roi, (vx_uint8)~FILL_A), + "the ROI node read the image the ROI was created from"); + vx_reference deq = nullptr; + vx_uint32 num = 0; + EXPECT_STATUS(vxGraphParameterDequeueDoneRef(graph, 0, &deq, 1, &num), VX_SUCCESS, + "the reference comes back"); + EXPECT_TRUE(num == 1 && deq == enq, "and it is the one that was enqueued"); + } + + vxReleaseNode(&n_full); + vxReleaseNode(&n_roi); + vxReleaseImage(&out_full); + vxReleaseImage(&out_roi); + vxReleaseImage(&roi); + vxReleaseImage(&master); + vxReleaseImage(&master2); + vxReleaseGraph(&graph); + vxReleaseContext(&context); + return errors; +} + +// --------------------------------------------------------------------------- +// Test 23: a streaming graph driven by hand +// +// With streaming enabled the graph belongs to the streaming thread. An explicit +// execution request is not an error, it simply has nothing of its own to do. +// --------------------------------------------------------------------------- +static int test_streaming_explicit_execution() +{ + int errors = 0; + printf("\n=== Test 23: explicit execution of a streaming graph ===\n"); + + vx_context context = vxCreateContext(); + NotGraph g = make_not_graph(context, 0); + CHECK_STATUS(fill_u8_image(g.in, 0x88)); + + EXPECT_STATUS(vxEnableGraphStreaming(g.graph, g.node), VX_SUCCESS, "streaming enabled"); + EXPECT_STATUS(vxVerifyGraph(g.graph), VX_SUCCESS, "vxVerifyGraph"); + + // Streaming has not been started, so this is the application's own request. + EXPECT_STATUS(vxProcessGraph(g.graph), VX_SUCCESS, + "an explicit execution of a streaming graph is accepted"); + + // Nothing is in flight, so this has nothing to wait for and has to say so. + // Waiting for a streaming thread that was never started would never return. + EXPECT_STATUS(vxWaitGraph(g.graph), VX_SUCCESS, + "waiting on a graph whose streaming never started returns"); + + // Now let the streaming thread run for long enough to get through many + // iterations of its loop, then stop it the ordinary way. + EXPECT_STATUS(vxStartGraphStreaming(g.graph), VX_SUCCESS, "streaming started"); + std::this_thread::sleep_for(std::chrono::milliseconds(200)); + EXPECT_STATUS(vxStopGraphStreaming(g.graph), VX_SUCCESS, "streaming stopped"); + + // The output has been written by the streaming thread. + EXPECT_TRUE(u8_image_is_uniform(g.out, (vx_uint8)~0x88), + "the streaming thread executed the graph"); + + release_not_graph(g); + vxReleaseContext(&context); + return errors; +} + // --------------------------------------------------------------------------- // main // --------------------------------------------------------------------------- @@ -1070,6 +2019,17 @@ int main() printf("OpenVX Graph Pipelining Extension API Coverage Test\n"); printf("===================================================\n"); + // Every wait in this test is bounded, so the whole run is bounded too. If one + // of them ever stops being bounded, say which test was running rather than + // leaving a build to sit on it until the job is killed. + std::thread watchdog([]() { + std::this_thread::sleep_for(std::chrono::seconds(300)); + printf("\nFATAL: the test suite is stuck; the output above ends at the test that hung\n"); + fflush(stdout); + abort(); + }); + watchdog.detach(); + // The extension can be compiled out, in which case every entry point reports // VX_ERROR_NOT_SUPPORTED and there is nothing here to check. { @@ -1101,6 +2061,19 @@ int main() total_errors += test_streaming(); total_errors += test_queue_auto(); total_errors += test_concurrent_refs_handover(); + total_errors += test_post_verify_config_validation(); + total_errors += test_add_references_validation(); + total_errors += test_unqueued_parameters(); + total_errors += test_timeouts_expire(); + total_errors += test_node_error_events(); + total_errors += test_enqueue_count_lifecycle(); + total_errors += test_auto_schedule_and_teardown(); + total_errors += test_kernel_surface(); + total_errors += test_graph_event_null_handling(); + total_errors += test_node_events_under_pipelining(); + total_errors += test_user_node_under_pipelining(); + total_errors += test_roi_of_queued_image(); + total_errors += test_streaming_explicit_execution(); printf("\n===================================================\n"); if (total_errors == 0) {