Phase 10: Audit engine reliability and strengthen lifecycle regression tests
This commit is contained in:
parent
4eb2080933
commit
8dd96d3cc5
13 changed files with 394 additions and 5 deletions
16
Makefile
16
Makefile
|
|
@ -241,3 +241,19 @@ test-recovery: $(BUILD_DIR)/test_recovery
|
|||
./$(BUILD_DIR)/test_recovery
|
||||
|
||||
-include $(BUILD_DIR)/tests/integration/test_performance.d $(BUILD_DIR)/tests/integration/test_recovery.d
|
||||
|
||||
# Linux/GNU-compatible linker test; no instrumentation in production objects.
|
||||
$(BUILD_DIR)/test_reliability: $(BUILD_DIR)/tests/integration/test_reliability.o $(BUILD_DIR)/libxtc.a
|
||||
$(CC) $(LDFLAGS) -o $@ $^ -lm -Wl,--wrap=malloc,--wrap=calloc,--wrap=realloc,--wrap=free
|
||||
|
||||
.PHONY: test-reliability test-sanitize analyze
|
||||
test-reliability: test $(BUILD_DIR)/test_reliability
|
||||
./$(BUILD_DIR)/test_reliability
|
||||
|
||||
test-sanitize:
|
||||
ASAN_OPTIONS=detect_leaks=1:halt_on_error=1 UBSAN_OPTIONS=halt_on_error=1 $(MAKE) BUILD_DIR="$(BUILD_DIR)/sanitize" CC=clang CFLAGS="-O1 -g -std=c99 -Wall -Wextra -Wpedantic -Wconversion -Wshadow -Iinclude -fsanitize=address,undefined -fno-omit-frame-pointer" LDFLAGS="-fsanitize=address,undefined" test-reliability test-recovery
|
||||
|
||||
analyze:
|
||||
$(PYTHON) tools/analyze.py $(CORE_SOURCES)
|
||||
|
||||
-include $(BUILD_DIR)/tests/integration/test_reliability.d
|
||||
|
|
|
|||
|
|
@ -273,3 +273,10 @@ processing. Callback errors silence the block and reset the failing processor,
|
|||
without rolling back already advanced DSP. Native event instruments can render
|
||||
explicitly into graph instrument buffers. See NATIVE_API.md for exact lifecycle
|
||||
and serialization limits and PERFORMANCE.md for measured workload and budgets.
|
||||
|
||||
## Phase 10 ownership audit
|
||||
|
||||
[RELIABILITY.md](RELIABILITY.md) records resource owners, publication and
|
||||
reclamation ordering, callback work bounds and repeatable diagnostic tests.
|
||||
Graph stereo rendering completes both channels before writing caller outputs;
|
||||
either output may equal a source, but the output buffers must not overlap.
|
||||
|
|
|
|||
|
|
@ -222,3 +222,13 @@ No-JACK core/GUI/install checks and Clang ASan/UBSan core/GUI checks also passed
|
|||
(the GUI retains only its existing fontconfig leak suppression). TSan recorder
|
||||
and realtime-handoff checks passed. Two independent optimized source exports
|
||||
and their repacked source archives were byte-identical on this toolchain.
|
||||
|
||||
## Phase 10 reliability audit
|
||||
|
||||
Run `make test-reliability` for the core suites, allocation-failure sweep,
|
||||
callback allocation/free guard and native reclamation checks. Run
|
||||
`make test-sanitize` for Clang ASan/UBSan/leak checks and `make analyze` for
|
||||
Clang static analysis of all core sources. These need installed development
|
||||
tools; the allocation guard uses a GNU-compatible linker. See
|
||||
[RELIABILITY.md](RELIABILITY.md) for ownership, audit findings,
|
||||
full verification commands and remaining limits.
|
||||
|
|
|
|||
116
docs/RELIABILITY.md
Normal file
116
docs/RELIABILITY.md
Normal file
|
|
@ -0,0 +1,116 @@
|
|||
# Engine correctness and reliability
|
||||
|
||||
Phase 10 audits the existing engine rather than adding musical features. The
|
||||
review covers graph edits/compilation, offline DSP, plan publication, JACK
|
||||
callbacks/lifecycle, audio/MIDI source plans and the recorder handoff.
|
||||
|
||||
## Ownership and synchronization
|
||||
|
||||
| Resource | Owner and lifetime |
|
||||
| --- | --- |
|
||||
| Editable graph, project, history and asset paths | One serialized control thread (the GUI thread in the application). Borrowed model pointers expire on mutation/destruction. Never traverse these objects in the callback. |
|
||||
| Compiled graph routing/specifications | Copied during compilation; independent of the editable graph. Immutable after publication. All nodes, including disconnected nodes, are scheduled; only explicit outputs (or compatibility terminals) are mixed. |
|
||||
| DSP buffers, native instances, phases, meter peaks and graph-local clock | One processing consumer. Reset and meter reads require that owner or quiescence; these are not atomic GUI snapshots. Native callback code must remain loaded until all instances are destroyed. |
|
||||
| Pending graph | Single control producer publishes with release; the consumer acquires it at a block boundary. The revision is read before releasing the pending slot, preventing producer reuse of its metadata. A full pending slot rejects submission. |
|
||||
| Active graph | Consumer only until the consumer is stopped/joined. No producer access to mutable DSP state. |
|
||||
| Retired graph | Consumer releases only after its final access. Control acquires/exchanges the slot and destroys the plan. If the slot is occupied, adoption waits while processing the current graph continues. The callback never destroys plans. |
|
||||
| Realtime status | Individual atomic counters/revisions; a status read is not a transaction spanning every field. Required pointer/integer atomics must be always lock-free. |
|
||||
| JACK client, ports and backend control fields | Serialized control thread. JACK owns callback scheduling. Successful deactivate establishes processing quiescence; successful close synchronizes callbacks before freeing context. Failed deactivate/close preserves ownership for retry. Server-loss notification alone is not permission to free context. |
|
||||
| Preloaded audio/MIDI plans and source buffers | Backend owns them; prepare/replace while stopped, callback alone renders while running. MIDI voices/panic state follow the same rule; panic requests use atomics. |
|
||||
| Recorder | Caller owns it; backend borrows it. Attach/detach only while stopped. Audio is sole ring producer, control is sole drain/file writer. Acquire/release indices protect samples; errors are sticky. Detach/quiesce before finish/destroy. |
|
||||
| Widgets and GUI timers | GUI thread only; status polling and retired-plan collection occur there, never through FLTK calls from audio. |
|
||||
|
||||
The public realtime API is SPSC: multiple producers, simultaneous process calls,
|
||||
collection from arbitrary threads, and destruction during processing are not
|
||||
supported. Atomics do not remove those ownership requirements.
|
||||
|
||||
## Bounded callback work
|
||||
|
||||
Compilation, topological sorting, asset I/O, allocation and native preparation
|
||||
are control-thread operations. Graph processing traverses at most 256 nodes,
|
||||
65536 frames per accepted block and the precomputed edges (at most 256 squared
|
||||
storage entries). Graph edits validate port direction/type/layout and reject
|
||||
cycles transactionally using Kahn ordering; fixed public mono ports have no
|
||||
implicit event/audio conversions. Stereo uses two mono graph passes, independent
|
||||
native instances and one oscillator phase advance per frame.
|
||||
|
||||
Audio regions, MIDI spans, voices, events and automation are bounded by public
|
||||
project/MIDI capacities. Rendering scans bounded region/note arrays; automation
|
||||
uses binary searches in prepared lanes. JACK MIDI accepts at most 256 events per
|
||||
block; overflow requests panic and reports drops. Recorder push performs at most
|
||||
one ring-capacity-sized copy and never drains a file. No callback path waits for
|
||||
a producer, collector, GUI or disk. Realtime pointer handoff has no retry loop.
|
||||
|
||||
Oversized JACK blocks are silenced without graph/source processing. Clearing
|
||||
backend-provided output and computing peaks still costs the server's frame
|
||||
count; the server's buffer contract is trusted. Public render APIs likewise
|
||||
require caller-provided valid buffers. Lock-free does not promise a hardware
|
||||
latency bound for atomic instructions, libc memory functions or libm. Native
|
||||
callbacks are trusted: the host cannot prevent their allocation, blocking,
|
||||
crashes or excessive runtime. Maximum supported capacities are safety bounds,
|
||||
not a promise that every workload meets its audio deadline.
|
||||
|
||||
## Findings and regression coverage
|
||||
|
||||
Graph rendering previously mixed the left output before reading right-channel
|
||||
sources. With external input equal to the left output, the right pass could
|
||||
read already processed samples (gain applied twice). Both passes now finish
|
||||
source reads before either output is written. Tests check mono input aliasing
|
||||
and cross-channel source aliasing against exact expected samples. Output buffers
|
||||
must not overlap each other. A redundant assignment in missing-asset preparation
|
||||
was also removed after Clang's dead-store diagnostic; behavior is unchanged.
|
||||
|
||||
`test_reliability` uses GNU-compatible linker allocation interposition in the
|
||||
test executable only. It rejects malloc/calloc/realloc/free during rendering,
|
||||
fails each preparation allocation in turn until compilation succeeds, verifies
|
||||
that failed replacement preserves output and revisions, and counts both stereo
|
||||
native instances through retirement, collection and destruction of a pending
|
||||
plan. It repeats this lifecycle 100 times. A maximum 256-node reverse-insertion
|
||||
chain checks dependency scheduling, long-cycle rejection/rollback and compiled
|
||||
snapshot independence. The pthread handoff test submits 2000 native graphs
|
||||
while processing, checks whole-block output/revision consistency, asserts that
|
||||
prepare/destruction run on the control thread, and balances all native lifetimes
|
||||
after joining the consumer.
|
||||
|
||||
Existing backend fault tests cover repeated start/stop, partial registration,
|
||||
callback setup, open/activate/deactivate/close failures and retries, server loss,
|
||||
rate/buffer changes, missing buffers, MIDI disconnect/panic, and recorder detach.
|
||||
Other suites cover deterministic variable-block DSP, finite-output handling,
|
||||
project errors, record-ring saturation and concurrent draining, and crash/write
|
||||
failure preservation. No critical realtime safety defect remains known within
|
||||
these contracts; these tests are evidence for tested paths, not a formal proof.
|
||||
|
||||
## Reproducing the audit
|
||||
|
||||
From the repository root:
|
||||
|
||||
```sh
|
||||
make BUILD_DIR=build/audit all gui test-reliability test-recovery
|
||||
make BUILD_DIR=build/audit test-sanitize
|
||||
make analyze
|
||||
make BUILD_DIR=build/audit test-gui test-gui-jack test-jack test-jack-audio test-jack-midi
|
||||
make BUILD_DIR=build/nojack PKG_CONFIG=false all test-reliability gui
|
||||
```
|
||||
|
||||
`test-sanitize` needs installed Clang and runs core, allocation-failure and recovery
|
||||
tests with ASan/UBSan and Linux's default ASan leak checking. `analyze` uses
|
||||
Clang's path-sensitive analyzer on every core translation unit, including JACK
|
||||
when its development files are available, and fails on diagnostics. Python and
|
||||
Clang are development tools, not new runtime dependencies. The allocation guard
|
||||
requires a GNU-compatible linker. Live tests require an existing JACK-compatible
|
||||
server; GUI tests use installed FLTK and Xvfb. No target installs dependencies or
|
||||
changes server/system configuration.
|
||||
|
||||
Phase 10 verification on GCC 16.2.1 / Clang 23.1.3 passed normal and no-JACK builds,
|
||||
all core tests, ASan/UBSan/leak checks, Clang static analysis, GUI/mock and live
|
||||
JACK integrations, plus GCC TSan on the pthread native-publication and recorder
|
||||
concurrency tests. The GUI also passed ASan/UBSan with the existing
|
||||
`tests/integration/lsan.supp` fontconfig-only suppression; core tests use no leak
|
||||
suppression. The live graph test observed both replacement gains, ten restart
|
||||
cycles and zero xruns during its five-second software run.
|
||||
|
||||
Physical audio devices, hard realtime scheduling, multi-hour endurance and
|
||||
arbitrary third-party native callbacks remain unqualified. Sanitizers change
|
||||
timing and should not be used to measure production realtime performance. See
|
||||
PERFORMANCE.md for workload measurements, RECOVERY.md for process-crash limits,
|
||||
and NATIVE_API.md for the trusted extension boundary.
|
||||
|
|
@ -136,6 +136,17 @@ physical-device/endurance qualification is claimed.
|
|||
- No known critical real-time safety defects remain.
|
||||
- Resource ownership and cleanup behavior are documented.
|
||||
|
||||
Implemented: graph/DSP and ownership audit; stereo source-alias correction with
|
||||
exact-output regression tests; maximum-size reverse-order graph/cycle rollback;
|
||||
allocation-failure sweeps and callback allocation/free guards; 2000 concurrent
|
||||
native-plan replacements with control-thread destruction and balanced lifetimes;
|
||||
repeatable ASan/UBSan/leak and Clang static-analysis targets. Core, no-JACK,
|
||||
GUI/mock/live JACK, recovery and TSan publication/recorder tests pass. See
|
||||
RELIABILITY.md for ownership, synchronization, bounded-work analysis, commands
|
||||
and evidence. Physical-device/endurance validation and trusted native callback
|
||||
behavior remain explicit limits; no critical realtime safety defect is known
|
||||
within the documented contracts.
|
||||
|
||||
## Phase 11 — Complete Minimal Music-Making Workflow
|
||||
|
||||
**Goal:** Ensure a user can create, save, reopen, and play a small musical project without manually editing project files.
|
||||
|
|
|
|||
|
|
@ -257,7 +257,7 @@ void xtc_plan_destroy(xtc_plan *plan);
|
|||
* Unconnected gain/meter/output inputs receive external input. */
|
||||
/* Separate mono source buffers; NULL is silence. Existing wrappers omit MIDI. */
|
||||
/* Stereo graph with paired source buffers and a mono external input duplicated
|
||||
* to both sides. Output buffers must be distinct and must not alias sources.
|
||||
* to both sides. Output buffers must not overlap each other; either may equal a source.
|
||||
* Stereo oscillator phase advances once per frame. NULL right selects mono. */
|
||||
/**
|
||||
* @brief Processes a block with separate stereo timeline and instrument sources.
|
||||
|
|
|
|||
|
|
@ -169,7 +169,6 @@ xtc_result xtc_audio_plan_create(const xtc_project *project, unsigned int rate,
|
|||
if (!plan->first_issue[0])
|
||||
strcpy(plan->first_issue, clip->asset_path);
|
||||
}
|
||||
result = XTC_OK;
|
||||
total += plan->waves[a].frames;
|
||||
if (total > 67108864U) {
|
||||
result = XTC_ERR_CAPACITY;
|
||||
|
|
|
|||
|
|
@ -208,7 +208,6 @@ xtc_result xtc_plan_process_stereo(xtc_plan *plan, const float *input, const flo
|
|||
for (channel = 0; channel < (right ? 2U : 1U); ++channel) {
|
||||
const float *audio_source = channel ? timeline_right : timeline;
|
||||
const float *midi_source = channel ? instrument_right : instrument;
|
||||
float *destination = channel ? right : output;
|
||||
for (index = 0U; index < plan->node_count; ++index) {
|
||||
size_t node = plan->order[index];
|
||||
xtc_node spec = plan->nodes[node];
|
||||
|
|
@ -261,6 +260,10 @@ xtc_result xtc_plan_process_stereo(xtc_plan *plan, const float *input, const flo
|
|||
if (spec.type == XTC_NODE_METER)
|
||||
plan->peaks[node] = channel && plan->peaks[node] > peak ? plan->peaks[node] : peak;
|
||||
}
|
||||
}
|
||||
/* Finish both channels before writing either caller buffer. */
|
||||
for (channel = 0; channel < (right ? 2U : 1U); ++channel) {
|
||||
float *destination = channel ? right : output;
|
||||
for (frame = 0U; frame < frame_count; ++frame) {
|
||||
float sample = 0.0f;
|
||||
for (index = 0U; index < plan->node_count; ++index)
|
||||
|
|
|
|||
|
|
@ -182,3 +182,13 @@ No-JACK core/GUI/install checks and Clang ASan/UBSan core/GUI checks also passed
|
|||
(the GUI retains only its existing fontconfig leak suppression). TSan recorder
|
||||
and realtime-handoff checks passed. Two independent optimized source exports
|
||||
and their repacked source archives were byte-identical on this toolchain.
|
||||
|
||||
## Phase 10 reliability audit
|
||||
|
||||
Run `make test-reliability` for the core suites, allocation-failure sweep,
|
||||
callback allocation/free guard and native reclamation checks. Run
|
||||
`make test-sanitize` for Clang ASan/UBSan/leak checks and `make analyze` for
|
||||
Clang static analysis of all core sources. These need installed development
|
||||
tools; the allocation guard uses a GNU-compatible linker. See
|
||||
[RELIABILITY.md](../../docs/RELIABILITY.md) for ownership, audit findings,
|
||||
full verification commands and remaining limits.
|
||||
|
|
|
|||
130
tests/integration/test_reliability.c
Normal file
130
tests/integration/test_reliability.c
Normal file
|
|
@ -0,0 +1,130 @@
|
|||
/* GNU ld allocation interposition: fail preparation at every allocation and
|
||||
* reject host allocation/reclamation inside processing. No production hooks. */
|
||||
#include "xtc/xtc_realtime.h"
|
||||
#include "xtc/xtc_native.h"
|
||||
#include <assert.h>
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
|
||||
void *__real_malloc(size_t size);
|
||||
void *__real_calloc(size_t count, size_t size);
|
||||
void *__real_realloc(void *pointer, size_t size);
|
||||
void __real_free(void *pointer);
|
||||
static int processing, fail_after = -1;
|
||||
static unsigned int prepared, destroyed;
|
||||
static int fail_allocation(void)
|
||||
{
|
||||
assert(!processing);
|
||||
if (fail_after < 0) return 0;
|
||||
if (fail_after == 0) return 1;
|
||||
--fail_after;
|
||||
return 0;
|
||||
}
|
||||
void *__wrap_malloc(size_t size) { return fail_allocation() ? NULL : __real_malloc(size); }
|
||||
void *__wrap_calloc(size_t count, size_t size) { return fail_allocation() ? NULL : __real_calloc(count, size); }
|
||||
void *__wrap_realloc(void *pointer, size_t size) { return fail_allocation() ? NULL : __real_realloc(pointer, size); }
|
||||
void __wrap_free(void *pointer) { assert(!processing); __real_free(pointer); }
|
||||
static xtc_result prepare(double rate, size_t frames, float parameter, void **state)
|
||||
{
|
||||
(void)rate; (void)frames;
|
||||
assert(!processing);
|
||||
*state = malloc(sizeof(float));
|
||||
if (!*state) return XTC_ERR_OUT_OF_MEMORY;
|
||||
*(float *)*state = parameter;
|
||||
++prepared;
|
||||
return XTC_OK;
|
||||
}
|
||||
static void reset(void *state) { (void)state; }
|
||||
static xtc_result process(void *state, const xtc_native_block *block)
|
||||
{
|
||||
size_t i;
|
||||
assert(processing);
|
||||
for (i = 0; i < block->frames; ++i)
|
||||
block->output[i] = block->input[i] * *(float *)state;
|
||||
return XTC_OK;
|
||||
}
|
||||
static void destroy(void *state)
|
||||
{
|
||||
assert(!processing);
|
||||
if (state) ++destroyed;
|
||||
free(state);
|
||||
}
|
||||
static void render(xtc_realtime *engine, float expected)
|
||||
{
|
||||
float input[17], left[17], right[17];
|
||||
size_t i;
|
||||
for (i = 0; i < 17; ++i) input[i] = 1;
|
||||
processing = 1;
|
||||
assert(xtc_realtime_process_stereo(engine, input, NULL, NULL, NULL, NULL, left, right, 17) == XTC_OK);
|
||||
processing = 0;
|
||||
for (i = 0; i < 17; ++i) assert(left[i] == expected && right[i] == expected);
|
||||
}
|
||||
/* Maximum-sized reverse-insertion chain: long cycle rollback must preserve
|
||||
* routing, and compiled snapshots survive destruction of the editable model. */
|
||||
static void graph_limits(void)
|
||||
{
|
||||
xtc_graph *graph = NULL;
|
||||
xtc_plan *plan = NULL;
|
||||
size_t nodes[256], i;
|
||||
float input[3] = {1, -0.5f, 0.25f}, output[3];
|
||||
assert(xtc_graph_create(256, &graph) == XTC_OK);
|
||||
for (i = 0; i < 256; ++i)
|
||||
assert(xtc_graph_add_gain(graph, 1, &nodes[i]) == XTC_OK);
|
||||
for (i = 255; i > 0; --i)
|
||||
assert(xtc_graph_connect(graph, nodes[i], nodes[i - 1]) == XTC_OK);
|
||||
assert(xtc_graph_connect(graph, nodes[0], nodes[255]) == XTC_ERR_GRAPH_CYCLE);
|
||||
assert(xtc_graph_validate(graph) == XTC_OK);
|
||||
assert(xtc_graph_compile(graph, 48000, 3, &plan) == XTC_OK);
|
||||
xtc_graph_destroy(graph);
|
||||
processing = 1;
|
||||
assert(xtc_plan_process(plan, input, output, 3) == XTC_OK);
|
||||
processing = 0;
|
||||
for (i = 0; i < 3; ++i) assert(output[i] == input[i]);
|
||||
xtc_plan_destroy(plan);
|
||||
}
|
||||
|
||||
int main(void)
|
||||
{
|
||||
graph_limits();
|
||||
xtc_native_descriptor descriptor = {XTC_NATIVE_ABI_VERSION, sizeof(descriptor),
|
||||
"reliability.gain", XTC_NATIVE_EFFECT, prepare, reset, process, destroy};
|
||||
xtc_graph *graph = NULL;
|
||||
size_t node;
|
||||
unsigned int iteration;
|
||||
assert(xtc_graph_create(1, &graph) == XTC_OK);
|
||||
assert(xtc_graph_add_native(graph, &descriptor, 2, &node) == XTC_OK);
|
||||
for (iteration = 0; iteration < 100; ++iteration) {
|
||||
xtc_realtime *engine = NULL;
|
||||
int failure, succeeded = 0;
|
||||
assert(xtc_realtime_create(48000, 17, &engine) == XTC_OK);
|
||||
assert(xtc_realtime_submit(engine, graph) == XTC_OK);
|
||||
render(engine, 2);
|
||||
for (failure = 0; failure < 64; ++failure) {
|
||||
xtc_result result;
|
||||
fail_after = failure;
|
||||
result = xtc_realtime_submit(engine, graph);
|
||||
fail_after = -1;
|
||||
if (result == XTC_OK) { succeeded = 1; break; }
|
||||
assert(result == XTC_ERR_OUT_OF_MEMORY);
|
||||
/* Failed replacement preserves the active DSP and revisions. */
|
||||
{
|
||||
xtc_realtime_status status;
|
||||
assert(xtc_realtime_get_status(engine, &status) == XTC_OK);
|
||||
assert(status.active_revision == 1 && status.submitted_revision == 1);
|
||||
}
|
||||
render(engine, 2);
|
||||
}
|
||||
assert(succeeded);
|
||||
render(engine, 2); /* Leaves previous native instances retired. */
|
||||
assert(destroyed + 4 == prepared);
|
||||
xtc_realtime_collect(engine);
|
||||
assert(destroyed + 2 == prepared);
|
||||
assert(xtc_realtime_submit(engine, graph) == XTC_OK);
|
||||
/* Destruction must also clean an unadopted pending plan. */
|
||||
xtc_realtime_destroy(engine);
|
||||
assert(prepared == destroyed);
|
||||
}
|
||||
xtc_graph_destroy(graph);
|
||||
puts("Allocation failures, callback allocation guard, and native reclamation passed.");
|
||||
return 0;
|
||||
}
|
||||
|
|
@ -79,6 +79,33 @@ static int routing(void)
|
|||
return 0;
|
||||
}
|
||||
|
||||
/* External mono input is read by both channels, including when left aliases it. */
|
||||
static int stereo_alias(void)
|
||||
{
|
||||
xtc_graph *graph = NULL;
|
||||
xtc_plan *plan = NULL;
|
||||
size_t gain, audio, output, i;
|
||||
float left[4] = {1, 2, 3, 4}, right[4];
|
||||
CHECK(xtc_graph_create(3, &graph) == XTC_OK);
|
||||
CHECK(xtc_graph_add_gain(graph, 2, &gain) == XTC_OK);
|
||||
CHECK(xtc_graph_add_audio(graph, &audio) == XTC_OK);
|
||||
CHECK(xtc_graph_add_output(graph, &output) == XTC_OK);
|
||||
CHECK(xtc_graph_connect(graph, gain, output) == XTC_OK);
|
||||
CHECK(xtc_graph_connect(graph, audio, output) == XTC_OK);
|
||||
CHECK(xtc_graph_compile(graph, 48000, 4, &plan) == XTC_OK);
|
||||
CHECK(xtc_plan_process_stereo(plan, left, NULL, NULL, NULL, NULL, left, right, 4) == XTC_OK);
|
||||
for (i = 0; i < 4; ++i)
|
||||
CHECK(left[i] == 2 * (float)(i + 1) && right[i] == left[i]);
|
||||
for (i = 0; i < 4; ++i) right[i] = 10 + (float)i;
|
||||
/* Cross-channel source alias must also retain the original source samples. */
|
||||
CHECK(xtc_plan_process_stereo(plan, NULL, right, left, NULL, NULL, left, right, 4) == XTC_OK);
|
||||
for (i = 0; i < 4; ++i)
|
||||
CHECK(left[i] == 10 + (float)i && right[i] == 2 * (float)(i + 1));
|
||||
xtc_plan_destroy(plan);
|
||||
xtc_graph_destroy(graph);
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int validation(void)
|
||||
{
|
||||
xtc_graph *graph = NULL;
|
||||
|
|
@ -133,6 +160,7 @@ int main(void)
|
|||
CHECK(oscillator() == 0);
|
||||
CHECK(routing() == 0);
|
||||
CHECK(validation() == 0);
|
||||
CHECK(stereo_alias() == 0);
|
||||
puts("All offline graph tests passed.");
|
||||
return 0;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,11 +1,46 @@
|
|||
#define _POSIX_C_SOURCE 200809L
|
||||
#include "xtc/xtc_realtime.h"
|
||||
#include "xtc/xtc_native.h"
|
||||
#include <assert.h>
|
||||
#include <pthread.h>
|
||||
#include <sched.h>
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#define CHECK(c) do { if (!(c)) { fprintf(stderr,"FAIL %s:%d: %s\n",__FILE__,__LINE__,#c); return 1; } } while (0)
|
||||
|
||||
/* Count native lifetimes on the serialized producer while a real consumer
|
||||
* traverses plans. Destruction on that consumer is always a contract violation. */
|
||||
static pthread_t control_thread;
|
||||
static unsigned int prepared, destroyed;
|
||||
static xtc_result prepare(double rate, size_t frames, float gain, void **state)
|
||||
{
|
||||
(void)rate; (void)frames;
|
||||
assert(pthread_equal(pthread_self(), control_thread));
|
||||
*state = malloc(sizeof(float));
|
||||
if (!*state) return XTC_ERR_OUT_OF_MEMORY;
|
||||
*(float *)*state = gain;
|
||||
++prepared;
|
||||
return XTC_OK;
|
||||
}
|
||||
static void reset(void *state) { (void)state; }
|
||||
static xtc_result process(void *state, const xtc_native_block *block)
|
||||
{
|
||||
size_t i;
|
||||
for (i = 0; i < block->frames; ++i)
|
||||
block->output[i] = block->input[i] * *(float *)state;
|
||||
return XTC_OK;
|
||||
}
|
||||
static void destroy(void *state)
|
||||
{
|
||||
assert(pthread_equal(pthread_self(), control_thread));
|
||||
if (state) ++destroyed;
|
||||
free(state);
|
||||
}
|
||||
static const xtc_native_descriptor descriptor = {
|
||||
XTC_NATIVE_ABI_VERSION, sizeof(xtc_native_descriptor), "concurrent.gain",
|
||||
XTC_NATIVE_EFFECT, prepare, reset, process, destroy
|
||||
};
|
||||
|
||||
typedef struct { xtc_realtime *engine; unsigned int stop, failed; } worker_state;
|
||||
static void *worker(void *context)
|
||||
{
|
||||
|
|
@ -40,9 +75,9 @@ static int run(void)
|
|||
CHECK(xtc_realtime_create(48000.0, 32U, &engine) == XTC_OK);
|
||||
CHECK(xtc_realtime_process(engine, input, output, 4U) == XTC_OK && output[0] == 0.0f);
|
||||
CHECK(xtc_graph_create(1U, &one) == XTC_OK);
|
||||
CHECK(xtc_graph_add_gain(one, 1.0f, &node) == XTC_OK);
|
||||
CHECK(xtc_graph_add_native(one, &descriptor, 1.0f, &node) == XTC_OK);
|
||||
CHECK(xtc_graph_create(1U, &two) == XTC_OK);
|
||||
CHECK(xtc_graph_add_gain(two, 2.0f, &node) == XTC_OK);
|
||||
CHECK(xtc_graph_add_native(two, &descriptor, 2.0f, &node) == XTC_OK);
|
||||
CHECK(xtc_realtime_submit(engine, one) == XTC_OK);
|
||||
CHECK(xtc_realtime_submit(engine, two) == XTC_ERR_CAPACITY);
|
||||
CHECK(xtc_realtime_get_status(engine, &status) == XTC_OK && status.submitted_revision == 1U && status.active_revision == 0U);
|
||||
|
|
@ -68,6 +103,7 @@ static int run(void)
|
|||
CHECK(state.failed == 0U);
|
||||
CHECK(xtc_realtime_get_status(engine, &status) == XTC_OK && status.processed_blocks > 3U);
|
||||
xtc_realtime_destroy(engine); /* Also cleans pending and retired plans. */
|
||||
CHECK(prepared == destroyed);
|
||||
xtc_graph_destroy(one); xtc_graph_destroy(two);
|
||||
for (i = 0U; i < 100U; ++i) {
|
||||
CHECK(xtc_realtime_create(48000.0, 4U, &engine) == XTC_OK);
|
||||
|
|
@ -80,6 +116,7 @@ static int run(void)
|
|||
}
|
||||
int main(void)
|
||||
{
|
||||
control_thread = pthread_self();
|
||||
CHECK(run() == 0);
|
||||
puts("Realtime handoff and concurrent replacement tests passed.");
|
||||
return 0;
|
||||
|
|
|
|||
22
tools/analyze.py
Normal file
22
tools/analyze.py
Normal file
|
|
@ -0,0 +1,22 @@
|
|||
#!/usr/bin/env python3
|
||||
"""Run Clang's path-sensitive analyzer; any diagnostic fails the audit."""
|
||||
import pathlib
|
||||
import shutil
|
||||
import subprocess
|
||||
import sys
|
||||
|
||||
if not shutil.which("clang"):
|
||||
sys.exit("clang is required for make analyze")
|
||||
jack = subprocess.run(["pkg-config", "--cflags", "jack"], capture_output=True, text=True) if shutil.which("pkg-config") else None
|
||||
for source in sys.argv[1:]:
|
||||
flags = []
|
||||
if source == "src/backend/jack_backend.c" and jack and jack.returncode == 0:
|
||||
import shlex
|
||||
flags = ["-DXTC_HAVE_JACK", *shlex.split(jack.stdout)]
|
||||
result = subprocess.run(["clang", "--analyze", "-std=c99", "-Iinclude",
|
||||
"-Xanalyzer", "-analyzer-output=text", *flags,
|
||||
str(pathlib.Path(source))], capture_output=True, text=True)
|
||||
if result.returncode or result.stderr:
|
||||
sys.stderr.write(result.stdout + result.stderr)
|
||||
sys.exit(result.returncode or 1)
|
||||
print("Clang static analysis passed for all core sources.")
|
||||
Loading…
Add table
Add a link
Reference in a new issue