Skip to content

Commit

Permalink
Implement low-latency mode as default
Browse files Browse the repository at this point in the history
  • Loading branch information
fwsGonzo committed Sep 13, 2024
1 parent 10eeaaf commit e0cf087
Show file tree
Hide file tree
Showing 10 changed files with 288 additions and 54 deletions.
9 changes: 4 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -63,13 +63,12 @@ static void add_coin(const Node& player) {
+ std::to_string(coins) + ((coins == 1) ? " coin" : " coins"));
}

extern "C" Variant _on_body_entered(Variant arg) {
Node player_node = arg.as_node();
if (player_node.get_name() != "Player") {
extern "C" Variant _on_body_entered(Node player) {
if (player.get_name() != "Player") {
return {};
}
Node(".").queue_free(); // Remove the current coin!
add_coin(player_node);
get_node().queue_free(); // Remove the current coin!
add_coin(player);
return {};
}
```
Expand Down
17 changes: 15 additions & 2 deletions program/cpp/api/string.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,10 @@
*/
union String {
constexpr String() {} // DON'T TOUCH
String(std::string_view value)
: m_idx(Create(value.data(), value.size())) {}
String(std::string_view value);
String &operator =(std::string_view value);
template <size_t N>
String(const char (&value)[N]);

// String operations
void append(const String &value);
Expand Down Expand Up @@ -43,6 +45,7 @@ union String {
private:
unsigned m_idx = -1;
};
using NodePath = String; // NodePath is compatible with String

inline Variant::Variant(const String &s) {
m_type = Variant::STRING;
Expand All @@ -59,3 +62,13 @@ inline String Variant::as_string() const {
}
return String::from_variant_index(v.i);
}

inline String::String(std::string_view value)
: m_idx(Create(value.data(), value.size())) {}
inline String &String::operator =(std::string_view value) {
m_idx = Create(value.data(), value.size());
return *this;
}
template <size_t N>
inline String::String(const char (&value)[N])
: m_idx(Create(value, N - 1)) {}
11 changes: 11 additions & 0 deletions src/guest_datatypes.h
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,18 @@ struct GDNativeVariant {
struct {
int32_t ivec2_int[2];
};
struct {
uint64_t object_id;
GodotObject *object_ptr;
};
};

godot::Object *to_object() const {
if (object_ptr == nullptr)
return nullptr;
return internal::get_object_instance_binding(object_ptr);
}

} __attribute__((packed));

// -= Guest Data Types =-
Expand Down
77 changes: 52 additions & 25 deletions src/sandbox.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -27,11 +27,13 @@ void Sandbox::_bind_methods() {
mi.name = "vmcall";
mi.return_val = PropertyInfo(Variant::OBJECT, "result");
ClassDB::bind_vararg_method(METHOD_FLAGS_DEFAULT, "vmcall", &Sandbox::vmcall, mi, DEFVAL(std::vector<Variant>{}));
ClassDB::bind_vararg_method(METHOD_FLAGS_DEFAULT, "vmcallv", &Sandbox::vmcallv, mi, DEFVAL(std::vector<Variant>{}));
}
ClassDB::bind_method(D_METHOD("vmcallable", "function", "args"), &Sandbox::vmcallable, DEFVAL(Array{}));

// Internal testing.
ClassDB::bind_method(D_METHOD("assault", "test", "iterations"), &Sandbox::assault);
ClassDB::bind_method(D_METHOD("has_function", "function"), &Sandbox::has_function);

// Properties.
ClassDB::bind_method(D_METHOD("set_max_refs", "max"), &Sandbox::set_max_refs);
Expand Down Expand Up @@ -124,7 +126,7 @@ void Sandbox::load(PackedByteArray &&buffer, const std::vector<std::string> *arg
machine_t &m = machine();

m.set_userdata(this);
this->m_current_state = &this->m_states.at(MAX_LEVEL);
this->m_current_state = &this->m_states[0]; // Set the current state to the first state

this->initialize_syscalls();

Expand Down Expand Up @@ -156,27 +158,49 @@ void Sandbox::load(PackedByteArray &&buffer, const std::vector<std::string> *arg
}

Variant Sandbox::vmcall_address(gaddr_t address, const Variant **args, GDExtensionInt arg_count, GDExtensionCallError &error) {
return this->vmcall_internal(address, args, arg_count, error);
error.error = GDEXTENSION_CALL_OK;
return this->vmcall_internal(address, args, arg_count);
}
Variant Sandbox::vmcall(const Variant **args, GDExtensionInt arg_count, GDExtensionCallError &error) {
if (arg_count < 1) {
error.error = GDEXTENSION_CALL_ERROR_TOO_FEW_ARGUMENTS;
error.argument = -1;
return Variant();
}
error.error = GDEXTENSION_CALL_OK;

const Variant &function = *args[0];
args += 1;
arg_count -= 1;
const String function_name = function.operator String();
return this->vmcall_internal(cached_address_of(function_name.hash(), function_name), args, arg_count);
}
Variant Sandbox::vmcallv(const Variant **args, GDExtensionInt arg_count, GDExtensionCallError &error) {
if (arg_count < 1) {
error.error = GDEXTENSION_CALL_ERROR_TOO_FEW_ARGUMENTS;
error.argument = -1;
return Variant();
}
error.error = GDEXTENSION_CALL_OK;

const Variant &function = *args[0];
args += 1;
arg_count -= 1;
const String function_name = function.operator String();
return this->vmcall_internal(cached_address_of(function_name.hash(), function_name), args, arg_count, error);
// Store use_native_args state and restore it after the call
Variant result;
auto old_use_native_args = this->m_use_native_args;
this->m_use_native_args = false;
result = this->vmcall_internal(cached_address_of(function_name.hash(), function_name), args, arg_count);
this->m_use_native_args = old_use_native_args;
return result;
}
Variant Sandbox::vmcall_fn(const StringName &function, const Variant **args, GDExtensionInt arg_count) {
if (this->m_throttled > 0) {
this->m_throttled--;
return Variant();
}
GDExtensionCallError error;
Variant result = this->vmcall_internal(cached_address_of(function.hash()), args, arg_count, error);
Variant result = this->vmcall_internal(cached_address_of(function.hash()), args, arg_count);
return result;
}
void Sandbox::setup_arguments_native(gaddr_t arrayDataPtr, GuestVariant *v, const Variant **args, int argc) {
Expand Down Expand Up @@ -211,12 +235,11 @@ void Sandbox::setup_arguments_native(gaddr_t arrayDataPtr, GuestVariant *v, cons
break;
}
case Variant::VECTOR2I: { // 8- or 16-byte structs can be passed in registers
machine.cpu.reg(index++) = inner->ivec2_int[0];
machine.cpu.reg(index++) = inner->ivec2_int[1];
machine.cpu.reg(index++) = inner->value; // 64-bit packed integers
break;
}
case Variant::OBJECT: { // Objects are represented as uintptr_t
godot::Object *obj = arg.operator godot::Object *();
godot::Object *obj = inner->to_object();
this->add_scoped_object(obj);
machine.cpu.reg(index++) = uintptr_t(obj); // Fits in a single register
break;
Expand Down Expand Up @@ -289,11 +312,14 @@ GuestVariant *Sandbox::setup_arguments(gaddr_t &sp, const Variant **args, int ar
// A0 is the return value (Variant) of the function
return &v[0];
}
Variant Sandbox::vmcall_internal(gaddr_t address, const Variant **args, int argc, GDExtensionCallError &error) {
Variant Sandbox::vmcall_internal(gaddr_t address, const Variant **args, int argc) {
//error.error = GDEXTENSION_CALL_OK;
//return Variant();

CurrentState &state = this->m_states[m_level];
const bool is_reentrant_call = m_level > 1;
m_level++;

// Scoped objects and owning tree node
state.reset(this->m_max_refs);
CurrentState *old_state = this->m_current_state;
Expand All @@ -306,17 +332,16 @@ Variant Sandbox::vmcall_internal(gaddr_t address, const Variant **args, int argc
GuestVariant *retvar = nullptr;
riscv::CPU<RISCV_ARCH> &cpu = m_machine->cpu;
auto &sp = cpu.reg(riscv::REG_SP);
m_level++;
// execute guest function
if (m_level == 1) {
if (!is_reentrant_call) {
cpu.reg(riscv::REG_RA) = m_machine->memory.exit_address();
// reset the stack pointer to its initial location
sp = m_machine->memory.stack_initial();
// set up each argument, and return value
retvar = this->setup_arguments(sp, args, argc);
// execute!
m_machine->simulate_with(get_instructions_max() << 30, 0u, address);
} else if (m_level > 1 && m_level < MAX_LEVEL) {
} else if (m_level < MAX_LEVEL) {
riscv::Registers<RISCV_ARCH> regs;
regs = cpu.registers();
// we are in a recursive call, so wait before setting exit address
Expand All @@ -336,7 +361,6 @@ Variant Sandbox::vmcall_internal(gaddr_t address, const Variant **args, int argc
// Restore the previous state
this->m_level--;
this->m_current_state = old_state;
error.error = GDEXTENSION_CALL_OK;
return result;

} catch (const std::exception &e) {
Expand All @@ -349,8 +373,6 @@ Variant Sandbox::vmcall_internal(gaddr_t address, const Variant **args, int argc
// TODO: Free the function arguments and return value? Will help keep guest memory clean

this->m_current_state = old_state;
error.error = GDEXTENSION_CALL_ERROR_INVALID_ARGUMENT;
error.argument = -1;
return Variant();
}
}
Expand Down Expand Up @@ -384,10 +406,11 @@ void RiscvCallable::call(const Variant **p_arguments, int p_argcount, Variant &r
for (int i = 0; i < p_argcount; i++) {
m_varargs_ptrs[m_varargs_base_count + i] = p_arguments[i];
}
r_return_value = self->vmcall_internal(address, m_varargs_ptrs.data(), total_args, r_call_error);
r_return_value = self->vmcall_internal(address, m_varargs_ptrs.data(), total_args);
} else {
r_return_value = self->vmcall_internal(address, p_arguments, p_argcount, r_call_error);
r_return_value = self->vmcall_internal(address, p_arguments, p_argcount);
}
r_call_error.error = GDEXTENSION_CALL_OK;
}

void Sandbox::print(std::string_view text) {
Expand Down Expand Up @@ -432,6 +455,11 @@ gaddr_t Sandbox::address_of(std::string_view name) const {
return machine().address_of(name);
}

bool Sandbox::has_function(const StringName &p_function) const {
const gaddr_t address = cached_address_of(p_function.hash(), p_function);
return address != 0x0;
}

int64_t Sandbox::get_heap_usage() const {
if (machine().has_arena()) {
return machine().arena().bytes_used();
Expand Down Expand Up @@ -466,11 +494,12 @@ std::optional<const Variant *> Sandbox::get_scoped_variant(unsigned index) const
return state().scoped_variants[index - 1];
}
Variant &Sandbox::get_mutable_scoped_variant(unsigned index) {
if (index == 0 || index > state().scoped_variants.size()) {
std::optional<const Variant *> var_opt = get_scoped_variant(index);
if (!var_opt.has_value()) {
ERR_PRINT("Invalid scoped variant index.");
throw std::runtime_error("Invalid scoped variant index.");
}
const godot::Variant *var = state().scoped_variants[index - 1];
const Variant *var = var_opt.value();
// Find the variant in the variants list
auto it = std::find_if(state().variants.begin(), state().variants.end(), [var](const Variant &v) {
return &v == var;
Expand Down Expand Up @@ -631,22 +660,20 @@ const SandboxProperty *Sandbox::find_property_or_null(const StringName &name) co

void SandboxProperty::set(Sandbox &sandbox, const Variant &value) {
if (m_setter_address == 0) {
ERR_PRINT("Sandbox: Setter not found for property: " + m_name);
ERR_PRINT("Sandbox: Setter was invalid for property: " + m_name);
return;
}
const Variant *args[] = { &value };
GDExtensionCallError error;
auto old_use_native_args = sandbox.get_use_native_args();
sandbox.set_use_native_args(false); // Always use Variant for properties
sandbox.vmcall_internal(m_setter_address, args, 1, error);
sandbox.vmcall_internal(m_setter_address, args, 1);
sandbox.set_use_native_args(old_use_native_args);
}

Variant SandboxProperty::get(const Sandbox &sandbox) const {
if (m_getter_address == 0) {
ERR_PRINT("Sandbox: Getter not found for property: " + m_name);
ERR_PRINT("Sandbox: Getter was invalid for property: " + m_name);
return Variant();
}
GDExtensionCallError error;
return const_cast<Sandbox &>(sandbox).vmcall_internal(m_getter_address, nullptr, 0, error);
return const_cast<Sandbox &>(sandbox).vmcall_internal(m_getter_address, nullptr, 0);
}
21 changes: 17 additions & 4 deletions src/sandbox.h
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,12 @@ class Sandbox : public Node {
/// @param error The error code, if any.
/// @return The return value of the function call.
Variant vmcall(const Variant **args, GDExtensionInt arg_count, GDExtensionCallError &error);
/// @brief Make a function call to a function in the guest by its name. Always use Variant values for arguments.
/// @param args The arguments to pass to the function, where the first argument is the name of the function.
/// @param arg_count The number of arguments.
/// @param error The error code, if any.
/// @return The return value of the function call.
Variant vmcallv(const Variant **args, GDExtensionInt arg_count, GDExtensionCallError &error);
/// @brief Make a function call to a function in the guest by its name.
/// @param function The name of the function to call.
/// @param args The arguments to pass to the function.
Expand Down Expand Up @@ -112,6 +118,11 @@ class Sandbox : public Node {
gaddr_t cached_address_of(int64_t hash) const;
gaddr_t cached_address_of(int64_t hash, const String &name) const;

/// @brief Check if a function exists in the guest program.
/// @param p_function The name of the function to check.
/// @return True if the function exists, false otherwise.
bool has_function(const StringName &p_function) const;

// -= Call State Management =-

/// @brief Get the current call state.
Expand Down Expand Up @@ -222,7 +233,7 @@ class Sandbox : public Node {

void assault(const String &test, int64_t iterations);
void print(std::string_view text);
Variant vmcall_internal(gaddr_t address, const Variant **args, int argc, GDExtensionCallError &error);
Variant vmcall_internal(gaddr_t address, const Variant **args, int argc);
machine_t &machine() { return *m_machine; }
const machine_t &machine() const { return *m_machine; }

Expand All @@ -248,7 +259,7 @@ class Sandbox : public Node {

bool m_last_newline = false;
uint8_t m_throttled = 0;
uint8_t m_level = 0;
uint8_t m_level = 1; // Current call level (0 is for initialization)
bool m_use_native_args = false;

// Stats
Expand All @@ -269,8 +280,10 @@ class Sandbox : public Node {
}
};
CurrentState *m_current_state = nullptr;
// State stack, with the permanent (initial) state at the end.
std::array<CurrentState, MAX_LEVEL + 1> m_states;
// State stack, with the permanent (initial) state at index 0.
// That means eg. static Variant values are held stored in the state at index 0,
// so that they can be accessed by future VM calls, and not lost when a call ends.
std::array<CurrentState, MAX_LEVEL+1> m_states;

// Properties
mutable std::vector<SandboxProperty> m_properties;
Expand Down
2 changes: 1 addition & 1 deletion src/sandbox_project_settings.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,7 @@ void SandboxProjectSettings::register_settings() {
#else
register_setting_plain(DOCKER_PATH, "docker", DOCKER_PATH_HINT, true);
#endif
register_setting_plain(NATIVE_TYPES, false, NATIVE_TYPES_HINT, false);
register_setting_plain(NATIVE_TYPES, true, NATIVE_TYPES_HINT, false);
}

template <typename TType>
Expand Down
11 changes: 8 additions & 3 deletions src/syscalls.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -1065,9 +1065,14 @@ APICALL(api_timer_periodic) {
timer->set_wait_time(interval);
timer->set_one_shot(oneshot);
Node *topnode = emu.get_tree_base();
topnode->add_child(timer);
timer->set_owner(topnode);
timer->start();
// Add the timer to the top node, as long as the Sandbox is in a tree.
if (topnode != nullptr) {
topnode->add_child(timer);
timer->set_owner(topnode);
timer->start();
} else {
timer->set_autostart(true);
}
// Copy the callback capture storage to the timer timeout callback.
PackedByteArray capture_data;
capture_data.resize(capture->size());
Expand Down
4 changes: 4 additions & 0 deletions tests/project.godot
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,10 @@ config/name="tests"
config/features=PackedStringArray("4.3", "Forward Plus")
config/icon="res://icon.svg"

[editor]

script/native_types=true

[editor_plugins]

enabled=PackedStringArray("res://addons/gut/plugin.cfg")
Loading

0 comments on commit e0cf087

Please sign in to comment.