Skip to content

Commit feca114

Browse files
committed
Update callback codec and workflow
1 parent dcd5a28 commit feca114

6 files changed

Lines changed: 319 additions & 65 deletions

File tree

‎.github/workflows/build-packages.yml‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ on:
66
push:
77
branches:
88
- '**'
9+
workflow_call:
910

1011
permissions:
1112
contents: read
Lines changed: 112 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,112 @@
1+
name: Release packages
2+
3+
run-name: Prepare LuaSF draft release for ${{ github.ref_name }}
4+
5+
on:
6+
push:
7+
tags:
8+
- 'v*'
9+
10+
permissions:
11+
contents: read
12+
13+
concurrency:
14+
group: release-${{ github.ref }}
15+
cancel-in-progress: false
16+
17+
jobs:
18+
build-packages:
19+
name: Build release packages
20+
permissions:
21+
contents: read
22+
uses: ./.github/workflows/build-packages.yml
23+
24+
create-release:
25+
name: Create draft release
26+
needs: build-packages
27+
runs-on: ubuntu-latest
28+
timeout-minutes: 15
29+
permissions:
30+
actions: read
31+
contents: write
32+
env:
33+
GH_TOKEN: ${{ github.token }}
34+
RELEASE_ASSETS_DIR: release-assets
35+
RELEASE_TAG: ${{ github.ref_name }}
36+
37+
steps:
38+
- name: Download Windows packages
39+
uses: actions/download-artifact@v8
40+
with:
41+
name: LuaSF-Windows-VS2022-x64
42+
path: ${{ env.RELEASE_ASSETS_DIR }}
43+
44+
- name: Download macOS packages
45+
uses: actions/download-artifact@v8
46+
with:
47+
name: LuaSF-macOS-AppleSilicon-arm64
48+
path: ${{ env.RELEASE_ASSETS_DIR }}
49+
50+
- name: Verify release assets
51+
shell: bash
52+
run: |
53+
set -euo pipefail
54+
55+
expected_files=(
56+
'LuaSF-source.zip'
57+
'LuaSF-embedded-Windows-x64-MSVC.zip'
58+
'LuaSF-extension-Windows-x64-MSVC.zip'
59+
'LuaSF-source.tar.gz'
60+
'LuaSF-embedded-macOS-ARM64-clang.tar.gz'
61+
'LuaSF-extension-macOS-ARM64-clang.tar.gz'
62+
)
63+
64+
for filename in "${expected_files[@]}"; do
65+
path="$RELEASE_ASSETS_DIR/$filename"
66+
if [ ! -f "$path" ]; then
67+
echo "Missing release asset: $path" >&2
68+
exit 1
69+
fi
70+
done
71+
72+
actual_file_count=$(find "$RELEASE_ASSETS_DIR" -mindepth 1 -maxdepth 1 -type f -print | wc -l | tr -d '[:space:]')
73+
actual_entry_count=$(find "$RELEASE_ASSETS_DIR" -mindepth 1 -maxdepth 1 -print | wc -l | tr -d '[:space:]')
74+
if [ "$actual_file_count" -ne "${#expected_files[@]}" ] || [ "$actual_entry_count" -ne "${#expected_files[@]}" ]; then
75+
echo "Expected exactly ${#expected_files[@]} release assets, found $actual_file_count files and $actual_entry_count total entries:" >&2
76+
find "$RELEASE_ASSETS_DIR" -mindepth 1 -maxdepth 1 -printf '%f\n' | sort >&2
77+
exit 1
78+
fi
79+
80+
- name: Reject an existing release
81+
shell: bash
82+
run: |
83+
set -euo pipefail
84+
85+
existing_releases=$(
86+
gh api --paginate --method GET \
87+
"repos/$GITHUB_REPOSITORY/releases?per_page=100" \
88+
--jq '.[] | [.tag_name, (.id | tostring), (.draft | tostring), .html_url] | @tsv' |
89+
awk -F '\t' -v tag="$RELEASE_TAG" '$1 == tag { print $2 "\t" $3 "\t" $4 }'
90+
)
91+
if [ -n "$existing_releases" ]; then
92+
echo "A release already exists for tag '$RELEASE_TAG'; refusing to overwrite it:" >&2
93+
printf '%s\n' "$existing_releases" >&2
94+
exit 1
95+
fi
96+
97+
- name: Create draft release with six assets
98+
shell: bash
99+
run: |
100+
set -euo pipefail
101+
102+
gh release create "$RELEASE_TAG" \
103+
"$RELEASE_ASSETS_DIR/LuaSF-source.zip" \
104+
"$RELEASE_ASSETS_DIR/LuaSF-embedded-Windows-x64-MSVC.zip" \
105+
"$RELEASE_ASSETS_DIR/LuaSF-extension-Windows-x64-MSVC.zip" \
106+
"$RELEASE_ASSETS_DIR/LuaSF-source.tar.gz" \
107+
"$RELEASE_ASSETS_DIR/LuaSF-embedded-macOS-ARM64-clang.tar.gz" \
108+
"$RELEASE_ASSETS_DIR/LuaSF-extension-macOS-ARM64-clang.tar.gz" \
109+
--repo "$GITHUB_REPOSITORY" \
110+
--verify-tag \
111+
--draft \
112+
--generate-notes

‎README.md‎

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -279,17 +279,23 @@ fun(
279279
}
280280
```
281281

282-
Arrays contain flat interleaved samples at 1-based Lua indices. Counts are measured in frames; array lengths are measured in samples. A non-`nil` input array has exactly `inputFrameCount * frameChannelCount` samples. At end of stream, input is `nil` and its frame count is zero. The output array is zero-initialized with exactly `outputFrameCount * frameChannelCount` samples before the call.
282+
Arrays are ordinary copying arrays containing flat interleaved samples at dense, 1-based Lua indices. They are not native buffer views, and the old `.value` / `.capacity` fields are not accepted. LuaSF reuses its input and output scratch tables between calls: it overwrites the complete current input, zeroes the complete current output capacity, and removes stale tail entries when a later block is shorter. These tables are callback-scoped scratch values and must not be retained or accessed after the callback returns. Counts are measured in frames; array lengths are measured in samples. A non-`nil` input array has exactly `inputFrameCount * frameChannelCount` samples. At end of stream, input is `nil` and its frame count is zero. The output array has exactly `outputFrameCount * frameChannelCount` samples before the call.
283283

284284
The callback must return a table containing the consumed `inputFrameCount` and produced `outputFrameCount`, both within the original capacities. If the result omits `outputFrames`, LuaSF copies samples from the possibly modified third argument; if it provides `outputFrames`, that dense array replaces the third argument. The selected output array must cover every produced sample and may not exceed the output capacity. Sparse arrays, non-array keys, invalid counts, overflow, and capacity violations are contract errors.
285285

286-
The audio thread tries to enter the Lua state without waiting. Lock contention bypasses only the current block and does not latch an error; after the lock is released, processing resumes. A Lua or protocol error latches the shared processor context, so the current and subsequent blocks bypass. Its fixed-capacity deferred error can be consumed once on a logic thread with `LuaSF_take_deferred_callback_error`.
286+
The audio thread uses the host's non-blocking serialized try-enter and never waits for the Lua VM. Lock contention dry-bypasses only the current block without latching an error: LuaSF copies as many input frames as the output capacity permits, while end of stream remains `nil` plus `0` frames and produces `0` frames. Processing resumes on later blocks after the VM becomes available. Hosts that need uninterrupted stateful processing must give the processor an execution state that is not shared with contending work.
287+
288+
After a successful try-enter, LuaSF pauses automatic GC only when it was running on entry. The RAII guard covers scratch-array population, the protected Lua call, and result parsing/copying, and restores the entry state on normal and exceptional exits; a collector that was already stopped is left stopped. Consequently, allocations made by this bridge cannot trigger automatic GC or finalizers on the audio thread. An effect callback must not explicitly run or restart GC, and must not rely on finalizers for real-time cleanup.
289+
290+
A Lua or protocol error latches the shared processor context, so the current and subsequent blocks use the same dry-bypass fallback. Its fixed-capacity deferred error can be consumed once on a logic thread with `LuaSF_take_deferred_callback_error`; error reporting and cleanup must not be performed from the audio callback itself.
287291

288292
### Callback state and host contract
289293

290294
LuaSF uses the `LUA_RIDX_MAINTHREAD` value as the identity for every registered `lua_State*`, including coroutine threads. Sessions, execution hooks, deferred errors, callback retention, quiescing, and shutdown therefore belong to the main state. Callback conversion automatically registers the currently executing coroutine and keeps it alive until shutdown, so a callback may be created from a coroutine stack and invoked later through the owning main state. If a host wants to pass a newly created coroutine directly to lifecycle or enter/leave APIs before any callback conversion has observed it, the host must first call `LuaSF_initialize_state(coroutine)` while it exclusively owns that VM; otherwise, pass the main state.
291295

292-
The host must serialize every Lua entry point—including `lua_pcall`, `luaL_dofile`, and equivalent APIs—with native callbacks. Wrap each entry with a successful `LuaSF_enter_state` and a matching `LuaSF_leave_state`, or install execution hooks and use that same re-entrant/recursive VM lock consistently for host execution. Before closing a state, stop callback producers and always perform `LuaSF_quiesce_state` → `LuaSF_shutdown_state` → `lua_close`; no callback may access the state after shutdown.
296+
The host must serialize every Lua entry point—including `lua_pcall`, `luaL_dofile`, and equivalent APIs—with native callbacks. Wrap each entry with a successful `LuaSF_enter_state` and a matching `LuaSF_leave_state`, or install execution hooks and use that same re-entrant/recursive VM lock consistently for host execution. The effect path intentionally uses only the non-blocking try-enter hook. An effect callback must never invoke a producer-waiting lifecycle operation on its own source, because that operation would wait for the current callback itself.
297+
298+
Before closing a state, stop callback producers and detach or destroy their processors/factories while the state is still valid, then always perform `LuaSF_quiesce_state` → `LuaSF_shutdown_state` → `lua_close`. Quiesce/shutdown is not a substitute for stopping producer threads, and no callback may access the state after shutdown.
293299

294300
## Runtime Notes
295301

‎README_zh_CN.md‎

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -279,17 +279,23 @@ fun(
279279
}
280280
```
281281

282-
数组按 Lua 的 1-based 索引保存 flat interleaved sample。count 的单位是 frame,数组长度的单位是 sample。非 `nil` 输入数组必须恰好包含 `inputFrameCount * frameChannelCount` 个 sample;流结束时 input 为 `nil` 且输入 frame count 必须为零。调用前,输出数组会按 `outputFrameCount * frameChannelCount` 个 sample 零初始化。
282+
数组是普通 copying array,按 Lua 的 dense、1-based 索引保存 flat interleaved sample;它们不是原生 buffer view,也不接受旧 `.value` / `.capacity` 字段。LuaSF 会在多次调用之间复用 input/output scratch table:每次覆盖完整的当前输入、把完整输出容量清零,并在后续 block 变短时删除旧的尾部条目。这些 table 只在本次 callback 期间有效,callback 返回后不得保留或访问。count 的单位是 frame,数组长度的单位是 sample。非 `nil` 输入数组必须恰好包含 `inputFrameCount * frameChannelCount` 个 sample;流结束时 input 为 `nil` 且输入 frame count 必须为零。调用前,输出数组恰好包含 `outputFrameCount * frameChannelCount` 个 sample。
283283

284284
callback 必须返回 table,其中包含实际消费的 `inputFrameCount` 和实际产生的 `outputFrameCount`,且二者不能超过原始容量。若结果省略 `outputFrames`,LuaSF 会回拷可能已被修改的第三个参数;若提供 `outputFrames`,则改用该 dense 数组。选中的输出数组必须覆盖所有实际输出 sample,且不能超过输出容量。稀疏数组、非数组 key、非法 count、溢出和容量越界均属于协议错误。
285285

286-
音频线程只尝试进入 Lua state,不会等待。锁竞争只会旁路当前 block,且不会锁存错误;锁释放后会恢复处理。Lua 或协议错误会锁存共享 processor context,使当前及后续 block 均旁路;逻辑线程可通过 `LuaSF_take_deferred_callback_error` 仅取出一次固定容量的延迟错误。
286+
音频线程使用宿主的非阻塞串行 try-enter,绝不会等待 Lua VM。锁竞争只会 dry-bypass 当前 block,且不会锁存错误:LuaSF 会在输出容量范围内原样复制尽可能多的输入 frame;流结束仍保持 `nil` 加 `0` 输入 frame,并产生 `0` frame。VM 可用后,后续 block 会恢复处理。需要保持有状态处理连续性的宿主必须给 processor 使用不与其他工作竞争的 execution state。
287+
288+
成功 try-enter 后,LuaSF 只在进入时自动 GC 正在运行的情况下暂停它。RAII 守卫覆盖 scratch array 填充、受保护 Lua 调用以及结果解析和回拷;正常与异常退出都会恢复进入时的状态,原本已停止的 collector 仍保持停止。因此,这一桥接过程中的内存分配不会在音频线程触发自动 GC 或 finalizer。音效 callback 不得显式执行或重启 GC,也不得依赖 finalizer 完成实时清理。
289+
290+
Lua 或协议错误会锁存共享 processor context,使当前及后续 block 使用相同的 dry-bypass fallback。逻辑线程可通过 `LuaSF_take_deferred_callback_error` 仅取出一次固定容量的延迟错误;不得在音频 callback 内报告错误或执行清理。
287291

288292
### Callback state 与宿主契约
289293

290294
LuaSF 以 `LUA_RIDX_MAINTHREAD` 作为每个已登记 `lua_State*`(包括 coroutine thread)的统一 identity。session、execution hook、deferred error、callback 保活、quiesce 和 shutdown 都归属 main state。callback 转换会自动登记当前正在执行的 coroutine,并将其强保活到 shutdown,因此 callback 可以在 coroutine 栈上创建 registry 引用,之后通过所属 main state 安全调用。如果宿主要在 callback 转换尚未观察到新 coroutine 前,直接把它传给 lifecycle 或 enter/leave API,必须先在独占该 VM 时调用 `LuaSF_initialize_state(coroutine)`;否则应传 main state。
291295

292-
宿主必须让所有 Lua 入口(包括 `lua_pcall`、`luaL_dofile` 及同类 API)与 native callback 串行化。每个入口都应在成功的 `LuaSF_enter_state` 后执行并匹配 `LuaSF_leave_state`,或者安装 execution hooks,并让宿主执行始终使用同一把可重入/递归 VM lock。关闭 state 前必须先停止 callback producer,并严格执行 `LuaSF_quiesce_state` → `LuaSF_shutdown_state` → `lua_close`;shutdown 之后 callback 不得再访问该 state。
296+
宿主必须让所有 Lua 入口(包括 `lua_pcall`、`luaL_dofile` 及同类 API)与 native callback 串行化。每个入口都应在成功的 `LuaSF_enter_state` 后执行并匹配 `LuaSF_leave_state`,或者安装 execution hooks,并让宿主执行始终使用同一把可重入/递归 VM lock。effect 路径有意只使用非阻塞 try-enter hook。effect callback 绝不得对自己所属的 source 调用需等待 producer 的生命周期操作,否则该操作会等待当前 callback 自身。
297+
298+
关闭 state 前必须先停止 callback producer,并在 state 仍有效时解除或销毁它们的 processor/factory,然后严格执行 `LuaSF_quiesce_state` → `LuaSF_shutdown_state` → `lua_close`。quiesce/shutdown 不能替代停止 producer thread;shutdown 之后 callback 不得再访问该 state。
293299

294300
## 运行时说明
295301

‎cmake/result_README.md‎

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,10 @@ Available CMake items:
3232

3333
Ordinary `std::function<R(Args...)>` parameters are bridged automatically when their nested argument and return types use the normal LuaSF value conversions; no SFML function-name whitelist is involved. Bare pointers, writable references/out parameters, native-thread boundaries, and other special lifetime or synchronization contracts require one explicit semantic-alias or exact-use-site codec selector. Lua values, including policy-controlled `nil`, are validated at the binding boundary. C++ APIs returning `std::function` are not generated in this release.
3434

35-
`sf.SoundSource.EffectProcessor` uses flat interleaved `number[]` values at 1-based indices. Lua receives `inputFrames: number[]|nil`, the input frame capacity, a zero-initialized output-capacity array, the output frame capacity, and the channel count. It must return `{ inputFrameCount: integer, outputFrameCount: integer, outputFrames: number[]? }`. Omitting `outputFrames` copies the modified third argument; providing it selects the replacement array. Arrays must be dense, counts must remain within their original capacities, the selected output must cover all produced samples without exceeding capacity, and end-of-stream input is `nil` with zero input frames. Audio-thread lock contention bypasses only the current block; Lua or protocol faults latch the shared processor and are retrieved once on a logic thread through `LuaSF_take_deferred_callback_error`.
35+
`sf.SoundSource.EffectProcessor` uses ordinary copying arrays of flat interleaved `number[]` values at dense, 1-based indices; native buffer views and the old `.value` / `.capacity` fields are not accepted. Lua receives `inputFrames: number[]|nil`, the input frame capacity, a zero-initialized output-capacity array, the output frame capacity, and the channel count. LuaSF reuses these scratch tables between calls, overwrites or zeroes their complete current ranges, and removes stale tails when a block becomes shorter, so callbacks must not retain them after returning. The callback must return `{ inputFrameCount: integer, outputFrameCount: integer, outputFrames: number[]? }`. Omitting `outputFrames` copies the modified third argument; providing it selects the replacement array. Counts are in frames, array lengths are in samples, arrays must be dense, the selected output must cover all produced samples without exceeding capacity, and end-of-stream input is `nil` with zero input frames.
3636

37-
LuaSF uses the owning main state as the identity for registered main-state and coroutine `lua_State*` values. Callback conversion registers and retains the current coroutine automatically; a newly created coroutine passed directly to lifecycle APIs must first be registered with `LuaSF_initialize_state(coroutine)` while the host exclusively owns the VM, or the host should pass the main state. Hosts must serialize every Lua entry such as `lua_pcall` and `luaL_dofile` with native callbacks by using `LuaSF_enter_state`/`LuaSF_leave_state`, or by consistently using installed execution hooks and the same re-entrant/recursive VM lock. Shutdown order is `LuaSF_quiesce_state` → `LuaSF_shutdown_state` → `lua_close`, after callback producers have stopped.
37+
The audio thread uses the host's non-blocking serialized try-enter and never waits for the Lua VM. Lock contention dry-bypasses only the current block without latching an error, copying as many input frames as output capacity permits; end of stream remains `nil` plus `0` and produces `0`. Hosts that require continuous stateful processing must use an execution state without competing work. After successful entry, LuaSF pauses automatic GC only if it was running, across scratch-array population, the Lua call, and result parsing, then restores the entry state on every exit. A collector already stopped stays stopped, so bridge allocations cannot trigger automatic GC or finalizers on the audio thread. Effect callbacks must not explicitly run/restart GC or rely on finalizers. Lua or protocol faults latch the shared processor into the same fallback and are retrieved once on a logic thread through `LuaSF_take_deferred_callback_error`.
38+
39+
LuaSF uses the owning main state as the identity for registered main-state and coroutine `lua_State*` values. Callback conversion registers and retains the current coroutine automatically; a newly created coroutine passed directly to lifecycle APIs must first be registered with `LuaSF_initialize_state(coroutine)` while the host exclusively owns the VM, or the host should pass the main state. Hosts must serialize every Lua entry such as `lua_pcall` and `luaL_dofile` with native callbacks by using `LuaSF_enter_state`/`LuaSF_leave_state`, or by consistently using installed execution hooks and the same re-entrant/recursive VM lock. The effect path intentionally uses only the non-blocking try-enter hook. An effect callback must never perform a producer-waiting lifecycle operation on its own source, because it would wait for the current callback itself.
40+
41+
Stop callback producers and detach/destroy their processors or factories while the state is still valid, then shut down in the order `LuaSF_quiesce_state` → `LuaSF_shutdown_state` → `lua_close`. Quiesce/shutdown does not stop producer threads, and callbacks must never access a shut-down state.

0 commit comments

Comments
 (0)