new evolved.process_with function impl

This commit is contained in:
BlackMATov
2026-01-03 15:42:59 +07:00
parent d42c0cf3db
commit 5e0170b4e8
6 changed files with 406 additions and 43 deletions

View File

@@ -43,6 +43,7 @@
- [Deferred Operations](#deferred-operations)
- [Batch Operations](#batch-operations)
- [Systems](#systems)
- [Processing Payloads](#processing-payloads)
- [Predefined Traits](#predefined-traits)
- [Fragment Tags](#fragment-tags)
- [Fragment Hooks](#fragment-hooks)
@@ -60,6 +61,7 @@
- [Chunk](#chunk)
- [Builder](#builder)
- [Changelog](#changelog)
- [vX.Y.Z](#vxyz)
- [v1.6.0](#v160)
- [v1.5.0](#v150)
- [v1.4.0](#v140)
@@ -880,6 +882,43 @@ The prologue and epilogue fragments do not require an explicit query. They will
> [!NOTE]
> And one more thing about systems. Execution callbacks are called in the [deferred scope](#deferred-operations), which means that all modifying operations inside the callback will be queued and applied after the system has processed all chunks. But prologue and epilogue callbacks are not called in the deferred scope, so all modifying operations inside them will be applied immediately. This is done to avoid confusion and to make it clear that prologue and epilogue callbacks are not part of the chunk processing.
#### Processing Payloads
Additionally, systems can have a payload that will be passed to the execution, prologue, and epilogue callbacks. This is useful for passing additional data to the system without using global variables or closures.
```lua
---@param system evolved.system
---@param ... any processing payload
function evolved.process_with(system, ...) end
```
The [`evolved.process_with`](#evolvedprocess_with) function is similar to the [`evolved.process`](#evolvedprocess) function, but it takes a processing payload as additional arguments. These arguments will be passed to the system's callbacks.
```lua
local evolved = require 'evolved'
local position_x, position_y = evolved.id(2)
local velocity_x, velocity_y = evolved.id(2)
local physics_system = evolved.builder()
:include(position_x, position_y)
:include(velocity_x, velocity_y)
:execute(function(chunk, entity_list, entity_count, delta_time)
local px, py = chunk:components(position_x, position_y)
local vx, vy = chunk:components(velocity_x, velocity_y)
for i = 1, entity_count do
px[i] = px[i] + vx[i] * delta_time
py[i] = py[i] + vy[i] * delta_time
end
end):build()
local delta_time = 0.016
evolved.process_with(physics_system, delta_time)
```
`delta_time` in this example is passed as a processing payload to the system's execution callback. Payloads can be of any type and can be multiple values. Also, payloads are passed to prologue and epilogue callbacks if they are defined. Every subsystem in a group will receive the same payload when the group is processed with [`evolved.process_with`](#evolvedprocess_with).
### Predefined Traits
#### Fragment Tags
@@ -1125,9 +1164,9 @@ storage :: component[]
default :: component
duplicate :: {component -> component}
execute :: {chunk, entity[], integer}
prologue :: {}
epilogue :: {}
execute :: {chunk, entity[], integer, any...}
prologue :: {any...}
epilogue :: {any...}
set_hook :: {entity, fragment, component, component}
assign_hook :: {entity, fragment, component, component}
@@ -1229,6 +1268,7 @@ execute :: query -> {execute_state? -> chunk?, entity[]?, integer?}, execute_sta
locate :: entity -> chunk?, integer
process :: system... -> ()
process_with :: system, ... -> ()
debug_mode :: boolean -> ()
collect_garbage :: ()
@@ -1302,16 +1342,20 @@ builder_mt:on_remove :: {entity, fragment} -> builder
builder_mt:group :: system -> builder
builder_mt:query :: query -> builder
builder_mt:execute :: {chunk, entity[], integer} -> builder
builder_mt:execute :: {chunk, entity[], integer, any...} -> builder
builder_mt:prologue :: {} -> builder
builder_mt:epilogue :: {} -> builder
builder_mt:prologue :: {any...} -> builder
builder_mt:epilogue :: {any...} -> builder
builder_mt:destruction_policy :: id -> builder
```
## Changelog
### vX.Y.Z
- Added the new [`evolved.process_with`](#evolvedprocess_with) function that allows passing payloads to processing systems
### v1.6.0
- Significant performance improvements of the [`evolved.REQUIRES`](#evolvedrequires) fragment trait
@@ -1710,6 +1754,14 @@ function evolved.locate(entity) end
function evolved.process(...) end
```
### `evolved.process_with`
```lua
---@param system evolved.system
---@param ... any processing payload
function evolved.process_with(system, ...) end
```
### `evolved.debug_mode`
```lua

View File

@@ -7,6 +7,7 @@ require 'develop.testing.locate_tests'
require 'develop.testing.main_tests'
require 'develop.testing.multi_spawn_tests'
require 'develop.testing.name_tests'
require 'develop.testing.process_with_tests'
require 'develop.testing.requires_fragment_tests'
require 'develop.testing.spawn_tests'
require 'develop.testing.system_as_query_tests'

View File

@@ -0,0 +1,107 @@
local evo = require 'evolved'
do
local f = evo.id()
local e = evo.builder():set(f, 42):spawn()
local s = evo.builder()
:include(f)
:prologue(function(payload1, payload2, payload3)
assert(payload1 == 11 and payload2 == 22 and payload3 == 33)
end)
:execute(function(chunk, entity_list, entity_count, payload1, payload2, payload3)
assert(payload1 == 11 and payload2 == 22 and payload3 == 33)
assert(chunk == evo.chunk(f) and entity_count == 1 and entity_list[1] == e)
end)
:epilogue(function(payload1, payload2, payload3)
assert(payload1 == 11 and payload2 == 22 and payload3 == 33)
end)
:spawn()
evo.process_with(s, 11, 22, 33)
end
do
local f = evo.id()
local e = evo.builder():set(f, 42):spawn()
local s = evo.builder()
:include(f)
:prologue(function(payload1, payload2, payload3)
assert(payload1 == nil and payload2 == 42 and payload3 == nil)
end)
:execute(function(chunk, entity_list, entity_count, payload1, payload2, payload3)
assert(payload1 == nil and payload2 == 42 and payload3 == nil)
assert(chunk == evo.chunk(f) and entity_count == 1 and entity_list[1] == e)
end)
:epilogue(function(payload1, payload2, payload3)
assert(payload1 == nil and payload2 == 42 and payload3 == nil)
end)
:spawn()
evo.process_with(s, nil, 42)
end
do
local f = evo.id()
local e = evo.builder():set(f, 42):spawn()
local s = evo.builder()
:include(f)
:prologue(function(payload1, payload2, payload3)
assert(payload1 == nil and payload2 == nil and payload3 == nil)
end)
:execute(function(chunk, entity_list, entity_count, payload1, payload2, payload3)
assert(payload1 == nil and payload2 == nil and payload3 == nil)
assert(chunk == evo.chunk(f) and entity_count == 1 and entity_list[1] == e)
end)
:epilogue(function(payload1, payload2, payload3)
assert(payload1 == nil and payload2 == nil and payload3 == nil)
end)
:spawn()
evo.process_with(s)
end
do
local f = evo.id()
local e = evo.builder():set(f, 42):spawn()
local prologue_sum, execute_sum, epilogue_sum = 0, 0, 0
local function sum(...)
local s = 0
for i = 1, select('#', ...) do
s = s + select(i, ...)
end
return s
end
local function iota(n)
if n == 0 then return end
return n, iota(n - 1)
end
local s = evo.builder()
:include(f)
:prologue(function(...)
prologue_sum = prologue_sum + sum(...)
end)
:execute(function(chunk, entity_list, entity_count, ...)
execute_sum = execute_sum + sum(...)
assert(chunk == evo.chunk(f) and entity_count == 1 and entity_list[1] == e)
end)
:epilogue(function(...)
epilogue_sum = epilogue_sum + sum(...)
end)
:spawn()
for n = 0, 50 do
prologue_sum, execute_sum, epilogue_sum = 0, 0, 0
evo.process_with(s, iota(n))
local expect_sum = (n * (n + 1)) / 2
assert(prologue_sum == expect_sum)
assert(execute_sum == expect_sum)
assert(epilogue_sum == expect_sum)
end
end

View File

@@ -79,10 +79,10 @@
group: function(self: Builder, group: System): Builder
query: function(self: Builder, query: Query): Builder
execute: function(self: Builder, execute: function(Chunk, {Entity}, integer)): Builder
execute: function(self: Builder, execute: function(Chunk, {Entity}, integer, ...: any)): Builder
prologue: function(self: Builder, prologue: function()): Builder
epilogue: function(self: Builder, epilogue: function()): Builder
prologue: function(self: Builder, prologue: function(...: any)): Builder
epilogue: function(self: Builder, epilogue: function(...: any)): Builder
destruction_policy: function(self: Builder, destruction_policy: Id): Builder
end
@@ -171,6 +171,7 @@
locate: function(entity: Entity): Chunk | nil, integer
process: function(...: System)
process_with: function(system: System, ...: any)
debug_mode: function(yesno: boolean)
collect_garbage: function()

View File

@@ -43,10 +43,11 @@ local evolved = {
---@alias evolved.execute fun(
--- chunk: evolved.chunk,
--- entity_list: evolved.entity[],
--- entity_count: integer)
--- entity_count: integer,
--- ...: any)
---@alias evolved.prologue fun()
---@alias evolved.epilogue fun()
---@alias evolved.prologue fun(...: any)
---@alias evolved.epilogue fun(...: any)
---@alias evolved.set_hook fun(
--- entity: evolved.entity,
@@ -207,7 +208,6 @@ local __lua_string_format = string.format
local __lua_table_concat = table.concat
local __lua_table_sort = table.sort
local __lua_tostring = tostring
local __lua_xpcall = xpcall
---@type fun(nseq?: integer): table
local __lua_table_new = (function()
@@ -338,6 +338,210 @@ local __lua_debug_traceback = (function()
end
end)()
---@type fun(f: function, e: function, ...): boolean, ...
local __lua_xpcall = (function()
-- https://github.com/BlackMATov/xpcall.lua
local builtin_xpcall = xpcall
---@diagnostic disable-next-line: redundant-parameter
if __lua_select(2, builtin_xpcall(function(a) return a end, function() end, 42)) == 42 then
-- use the built-in xpcall if it works with extra arguments as expected
return builtin_xpcall
end
local xpcall_function
local xpcall_argument_1, xpcall_argument_2
local xpcall_argument_3, xpcall_argument_4
local xpcall_argument_5, xpcall_argument_6
local xpcall_argument_7, xpcall_argument_8
local xpcall_argument_tail_list = {}
local xpcall_argument_tail_count = 0
local function call_xpcall_function_1()
return xpcall_function(
xpcall_argument_1)
end
local function call_xpcall_function_2()
return xpcall_function(
xpcall_argument_1, xpcall_argument_2)
end
local function call_xpcall_function_3()
return xpcall_function(
xpcall_argument_1, xpcall_argument_2,
xpcall_argument_3)
end
local function call_xpcall_function_4()
return xpcall_function(
xpcall_argument_1, xpcall_argument_2,
xpcall_argument_3, xpcall_argument_4)
end
local function call_xpcall_function_5()
return xpcall_function(
xpcall_argument_1, xpcall_argument_2,
xpcall_argument_3, xpcall_argument_4,
xpcall_argument_5)
end
local function call_xpcall_function_6()
return xpcall_function(
xpcall_argument_1, xpcall_argument_2,
xpcall_argument_3, xpcall_argument_4,
xpcall_argument_5, xpcall_argument_6)
end
local function call_xpcall_function_7()
return xpcall_function(
xpcall_argument_1, xpcall_argument_2,
xpcall_argument_3, xpcall_argument_4,
xpcall_argument_5, xpcall_argument_6,
xpcall_argument_7)
end
local function call_xpcall_function_8()
return xpcall_function(
xpcall_argument_1, xpcall_argument_2,
xpcall_argument_3, xpcall_argument_4,
xpcall_argument_5, xpcall_argument_6,
xpcall_argument_7, xpcall_argument_8)
end
local function call_xpcall_function_N()
return xpcall_function(
xpcall_argument_1, xpcall_argument_2,
xpcall_argument_3, xpcall_argument_4,
xpcall_argument_5, xpcall_argument_6,
xpcall_argument_7, xpcall_argument_8,
__lua_table_unpack(xpcall_argument_tail_list, 1, xpcall_argument_tail_count))
end
---@type fun(f: function, e: function, ...): boolean, ...
return function(f, e, ...)
local argument_count = __lua_select('#', ...)
if argument_count == 0 then
-- use the built-in xpcall without extra arguments
return builtin_xpcall(f, e)
end
xpcall_function = f
if argument_count <= 8 then
if argument_count <= 4 then
if argument_count <= 2 then
if argument_count <= 1 then
xpcall_argument_1 = ...
return builtin_xpcall(call_xpcall_function_1, e)
else
xpcall_argument_1, xpcall_argument_2 = ...
return builtin_xpcall(call_xpcall_function_2, e)
end
else
if argument_count <= 3 then
xpcall_argument_1, xpcall_argument_2,
xpcall_argument_3 = ...
return builtin_xpcall(call_xpcall_function_3, e)
else
xpcall_argument_1, xpcall_argument_2,
xpcall_argument_3, xpcall_argument_4 = ...
return builtin_xpcall(call_xpcall_function_4, e)
end
end
else
if argument_count <= 6 then
if argument_count <= 5 then
xpcall_argument_1, xpcall_argument_2,
xpcall_argument_3, xpcall_argument_4,
xpcall_argument_5 = ...
return builtin_xpcall(call_xpcall_function_5, e)
else
xpcall_argument_1, xpcall_argument_2,
xpcall_argument_3, xpcall_argument_4,
xpcall_argument_5, xpcall_argument_6 = ...
return builtin_xpcall(call_xpcall_function_6, e)
end
else
if argument_count <= 7 then
xpcall_argument_1, xpcall_argument_2,
xpcall_argument_3, xpcall_argument_4,
xpcall_argument_5, xpcall_argument_6,
xpcall_argument_7 = ...
return builtin_xpcall(call_xpcall_function_7, e)
else
xpcall_argument_1, xpcall_argument_2,
xpcall_argument_3, xpcall_argument_4,
xpcall_argument_5, xpcall_argument_6,
xpcall_argument_7, xpcall_argument_8 = ...
return builtin_xpcall(call_xpcall_function_8, e)
end
end
end
else
xpcall_argument_1, xpcall_argument_2,
xpcall_argument_3, xpcall_argument_4,
xpcall_argument_5, xpcall_argument_6,
xpcall_argument_7, xpcall_argument_8 = ...
end
xpcall_argument_tail_count = argument_count - 8
local argument_tail_list = xpcall_argument_tail_list
for i = 1, argument_count - 8, 8 do
local argument_remaining = argument_count - 8 - i + 1
if argument_remaining <= 4 then
if argument_remaining <= 2 then
if argument_remaining <= 1 then
argument_tail_list[i] = __lua_select(i + 8, ...)
else
argument_tail_list[i], argument_tail_list[i + 1] = __lua_select(i + 8, ...)
end
else
if argument_remaining <= 3 then
argument_tail_list[i], argument_tail_list[i + 1],
argument_tail_list[i + 2] = __lua_select(i + 8, ...)
else
argument_tail_list[i], argument_tail_list[i + 1],
argument_tail_list[i + 2], argument_tail_list[i + 3] = __lua_select(i + 8, ...)
end
end
else
if argument_remaining <= 6 then
if argument_remaining <= 5 then
argument_tail_list[i], argument_tail_list[i + 1],
argument_tail_list[i + 2], argument_tail_list[i + 3],
argument_tail_list[i + 4] = __lua_select(i + 8, ...)
else
argument_tail_list[i], argument_tail_list[i + 1],
argument_tail_list[i + 2], argument_tail_list[i + 3],
argument_tail_list[i + 4], argument_tail_list[i + 5] = __lua_select(i + 8, ...)
end
else
if argument_remaining <= 7 then
argument_tail_list[i], argument_tail_list[i + 1],
argument_tail_list[i + 2], argument_tail_list[i + 3],
argument_tail_list[i + 4], argument_tail_list[i + 5],
argument_tail_list[i + 6] = __lua_select(i + 8, ...)
else
argument_tail_list[i], argument_tail_list[i + 1],
argument_tail_list[i + 2], argument_tail_list[i + 3],
argument_tail_list[i + 4], argument_tail_list[i + 5],
argument_tail_list[i + 6], argument_tail_list[i + 7] = __lua_select(i + 8, ...)
end
end
end
end
return builtin_xpcall(call_xpcall_function_N, e)
end
end)()
---
---
---
@@ -840,6 +1044,7 @@ local __evolved_execute
local __evolved_locate
local __evolved_process
local __evolved_process_with
local __evolved_debug_mode
local __evolved_collect_garbage
@@ -3665,32 +3870,24 @@ function __iterator_fns.__execute_iterator(execute_state)
__release_table(__table_pool_tag.execute_state, execute_state, true)
end
---@type { [1]: evolved.query, [2]: evolved.execute }
local __query_execute_external_arguments = {}
---@param query? evolved.query
---@param execute? evolved.execute
local function __query_execute(query, execute)
-- we use the external arguments here to support lua 5.1 xpcall (which does not support argument passing)
-- also, we can not use upvalues directly, because the function may be called recursively in that case
-- storing the arguments in local variables makes them invulnerable to changes during recursive calls
query = query or __query_execute_external_arguments[1]
execute = execute or __query_execute_external_arguments[2]
---@param query evolved.query
---@param execute evolved.execute
---@param ... any processing payload
local function __query_execute(query, execute, ...)
for chunk, entity_list, entity_count in __evolved_execute(query) do
execute(chunk, entity_list, entity_count)
execute(chunk, entity_list, entity_count, ...)
end
end
---@param system evolved.system
local function __system_process(system)
---@param ... any processing payload
local function __system_process(system, ...)
---@type evolved.query?, evolved.execute?, evolved.prologue?, evolved.epilogue?
local query, execute, prologue, epilogue = __evolved_get(system,
__QUERY, __EXECUTE, __PROLOGUE, __EPILOGUE)
if prologue then
local success, result = __lua_xpcall(prologue, __lua_debug_traceback)
local success, result = __lua_xpcall(prologue, __lua_debug_traceback, ...)
if not success then
__error_fmt('system prologue failed: %s', result)
@@ -3700,8 +3897,7 @@ local function __system_process(system)
if execute then
__evolved_defer()
do
__query_execute_external_arguments[1], __query_execute_external_arguments[2] = query or system, execute
local success, result = __lua_xpcall(__query_execute, __lua_debug_traceback, query or system, execute)
local success, result = __lua_xpcall(__query_execute, __lua_debug_traceback, query or system, execute, ...)
if not success then
__evolved_cancel()
@@ -3727,7 +3923,7 @@ local function __system_process(system)
for subsystem_index = 1, group_subsystem_count do
local subsystem = subsystem_list[subsystem_index]
if not __evolved_has(subsystem, __DISABLED) then
__system_process(subsystem)
__system_process(subsystem, ...)
end
end
@@ -3736,7 +3932,7 @@ local function __system_process(system)
end
if epilogue then
local success, result = __lua_xpcall(epilogue, __lua_debug_traceback)
local success, result = __lua_xpcall(epilogue, __lua_debug_traceback, ...)
if not success then
__error_fmt('system epilogue failed: %s', result)
@@ -5092,14 +5288,25 @@ function __evolved_process(...)
if __freelist_ids[system_primary] ~= system then
__warning_fmt('the system (%s) is not alive and cannot be processed',
__id_name(system))
elseif __evolved_has(system, __DISABLED) then
-- the system is disabled, nothing to process
else
__system_process(system)
end
end
end
---@param system evolved.system
---@param ... any processing payload
function __evolved_process_with(system, ...)
local system_primary = system % 2 ^ 20
if __freelist_ids[system_primary] ~= system then
__error_fmt('the system (%s) is not alive and cannot be processed',
__id_name(system))
end
__system_process(system, ...)
end
---@param yesno boolean
function __evolved_debug_mode(yesno)
__debug_mode = yesno
@@ -6337,6 +6544,7 @@ evolved.execute = __evolved_execute
evolved.locate = __evolved_locate
evolved.process = __evolved_process
evolved.process_with = __evolved_process_with
evolved.debug_mode = __evolved_debug_mode
evolved.collect_garbage = __evolved_collect_garbage

View File

@@ -12,10 +12,6 @@ local STAGES = {
:build(),
}
local UNIFORMS = {
DELTA_TIME = 1.0 / 60.0,
}
local FRAGMENTS = {
POSITION_X = evolved.builder()
:name('FRAGMENTS.POSITION_X')
@@ -82,8 +78,7 @@ evolved.builder()
:group(STAGES.ON_UPDATE)
:include(FRAGMENTS.POSITION_X, FRAGMENTS.POSITION_Y)
:include(FRAGMENTS.VELOCITY_X, FRAGMENTS.VELOCITY_Y)
:execute(function(chunk, _, entity_count)
local delta_time = UNIFORMS.DELTA_TIME
:execute(function(chunk, _, entity_count, delta_time)
local screen_width, screen_height = love.graphics.getDimensions()
---@type number[], number[]
@@ -156,8 +151,7 @@ end
---@type love.update
function love.update(dt)
UNIFORMS.DELTA_TIME = dt
evolved.process(STAGES.ON_UPDATE)
evolved.process_with(STAGES.ON_UPDATE, dt)
end
---@type love.draw