RVScript

repository·master·Indexed 43 days ago

https://github.com/libriscv/rvscript

A low-latency RISC-V based scripting system for game engines featuring a fast virtual machine for high-frequency script execution. It includes a 'micro' environment for syscall-accelerated C++ binaries, support for dynamic calls to expose host-side C++ functions, and a Timers class for scheduling one-shot and periodic tasks.

Tokens
3.2K
Snippets
11
Records
14
Agent score
55%

What's inside RVScript

  1. Understand the micro code structure and API

    master

    The micro project implements syscall-accelerated C++ binaries for the RISC-V scripting backend. The codebase is organized as follows:

    • api/: Contains the public API and its inlined implementation.
    • api/syscalls.h: Defines the system call numbers.
    • libc/: Contains a minimal libc and overridden C++ headers designed to fast-path new and delete operations.
    • libc/include/syscall.hpp: Provides system call wrappers.
    • engine/mods/: Contains example binaries (e.g., hello_world).
    • scripts/: Contains the majority of the logic/scripts.

    Build Options: When using CMake, the RTTI_EXCEPT option can be enabled to support C++ RTTI and exceptions.

  2. Create a host-side API using Dynamic Calls

    master

    Dynamic calls allow you to expose host-side C++ functions to the RISC-V guest script using string names. This method is safer and easier to use than raw system calls.

    Workflow

    1. Define the call in C++: Use Script::set_dynamic_call to map a signature to a lambda or function. The signature should include the full type information to minimize ABI mistakes.
    2. Register in JSON: Add the mapping to programs/dynamic_calls.json to link a short name to the full C++ signature.
    3. Call from Script: The build system generates a wrapper, allowing you to call the function directly in your RISC-V code (e.g., sys_lazy();).

    Supported Argument Patterns

    • Simple types: int, void, etc.
    • Results: Use script.machine().set_result(value) to return values to the script.
    • Structs: Pass structs by reference/pointer using script.args<T>().
    • Buffers: Use riscv::Buffer to handle char* buffer, size_t length pairs, or std::string_view for a view of the memory.
    // 1. Define in C++
    Script::set_dynamic_call("object_id",
        [] (Script& script) {
            const auto [id] = script.args <int> ();
            script.machine().set_result(1234);
        });
    
    // 2. Register in dynamic_calls.json
    // "object_id": "int object_id(int)"
    
    // 3. Call in RISC-V script
    int id = object_id(some_arg);
  3. Use the micro calling convention

    master

    The micro environment follows the standard RISC-V calling convention. System calls and function calls share the same registers, with one key distinction: the A7 register is used to specify the system call number.

    You can use libc/include/syscall.hpp as a reference for available system calls.

  4. Install and set up RVScript

    master

    To get started with RVScript, ensure you have cmake, git, and a C++ compiler (g++ or Clang) installed on your system.

    1. Initialize dependencies by running the provided setup script:

      ./setup.sh

      This script ensures libriscv and OpenGL dependencies are properly initialized.

    2. If you are not on a Debian-based distribution, it is recommended to use distrobox to create an Ubuntu 22.04 environment to build the project.

    3. Build the engine by navigating to the engine directory and running the build script:

      cd engine
      bash build.sh

    To run benchmarks during the build process, use the BENCHMARK=1 environment variable.

    # Standard build
    cd engine
    bash build.sh
    
    # Build with benchmarks
    BENCHMARK=1 ./build.sh
  5. Build and run the engine

    master

    Once a RISC-V compiler is configured, you can build the engine and its associated script programs. The build script in the engine folder automatically builds script programs located in the programs folder and uses scripts from the engine/scripts folder.

    cd engine
    ./build.sh

    If you need to change which compiler is prioritized, you can modify programs/detect_compiler.sh.

  6. Configure a RISC-V toolchain

    master

    RVScript requires a RISC-V compiler to build script programs. You have two main options:

    Option 1: System Package (Simplest)

    Install the GNU RISC-V toolchain via your package manager. The build script supports GNU toolchain versions 10 and later.

    sudo apt install g++-12-riscv64-linux-gnu

    Note: If using glibc from a system toolchain, be cautious when mixing microthreads with C++ exceptions.

    For more performant and nimble executables, use the riscvXX-unknown-elf toolchain.

    1. Clone and build the toolchain:
      git clone https://github.com/riscv/riscv-gnu-toolchain.git
      cd riscv-gnu-toolchain
      git submodule update --depth 1 --init

    Install your system's GCC dependencies here

    ./configure --prefix=$HOME/riscv --with-arch=rv64g_zba_zbb_zbc_zbs --with-abi=lp64d make -j8

    2. Add the toolchain to your `PATH` in `~/.bashrc`:
     ```bash
     export PATH=$PATH:$HOME/riscv/bin
    1. Restart your terminal. The build script will automatically prefer this compiler if it is found in your PATH.
    # Example: Installing the newlib toolchain
    git clone https://github.com/riscv/riscv-gnu-toolchain.git
    cd riscv-gnu-toolchain
    git submodule update --depth 1 --init
    ./configure --prefix=$HOME/riscv --with-arch=rv64g_zba_zbb_zbc_zbs --with-abi=lp64d
    make -j8
  7. Live-debug with GDB

    master

    You can enable live-debugging for the engine and its programs using the DEBUG=1 flag.

    1. For programs: Run DEBUG=1 ./build.sh inside the programs folder to produce GDB-friendly binaries.
    2. For the engine: Run DEBUG=1 ./build.sh inside the engine folder to enable remote debugging. The engine will listen for a remote debugger on each breakpoint and attempt to start GDB automatically.

    To connect to the remote debugger manually, install gdb-multiarch and use:

    gdb-multiarch myprogram.elf
    target remote :2159
    # Enable remote debugging for the engine
    cd engine
    DEBUG=1 ./build.sh
    
    # Manual GDB connection
    gdb-multiarch myprogram.elf
    target remote :2159
  8. Set up the RISC-V compiler for micro

    master

    To compile micro binaries, you must have a RISC-V compiler installed. The build system expects the compiler to be located in ~/riscv. To ensure the compiler is accessible in your terminal, add the bin directory to your PATH environment variable.

    If you use bash, add the following line to your ~/.bashrc file and restart your terminal:

    export PATH=$PATH:$HOME/riscv/bin
  9. Troubleshoot micro execution and threading issues

    master

    Common issues when working with micro binaries:

    • Remote Execution: When executing a function on a remote machine, ensure the function you are calling exists on the target machine, not the machine initiating the call.
    • Register Clobbering with vmcall(): If you call vmcall() on a machine that is currently performing an operation, its registers may be clobbered. This causes the machine to resume at the end of the vmcall rather than where it left off. To avoid this, use preempt() when calling into a machine performing long-term operations.
    • Blocking the Main Thread: You cannot block the main thread because it is subject to being clobbered by other functions calling into the machine. The design only allows blocking other threads.
    • C++ Exceptions: Standard C++ exceptions are not supported because the full C++ standard library is not linked (to maintain efficiency). For high-performance C++ containers, it is recommended to use EASTL (a fork is available that supports RISC-V).
  10. Reference: Dynamic Call API Patterns

    master

    The following patterns demonstrate how to use Script::set_dynamic_call for different data types and requirements.

    // Simple void function
    Script::set_dynamic_call("void sys_lazy ()", [] (auto&) {
        strf::to(stdout)("I'm not doing much, tbh.\n");
    });
    
    // Function with integer argument and return value
    Script::set_dynamic_call("object_id",
        [] (Script& script) {
            const auto [id] = script.args <int> ();
            script.machine().set_result(1234);
        });
    
    // Function with struct by reference
    Script::set_dynamic_call("struct_by_ref",
        [] (Script& script) {
            struct Something { int a, b, c, d; };
            const auto [s] = script.args <Something> ();
        });
    
    // Function with buffer/string view (consumes two registers: A0=ptr, A1=len)
    Script::set_dynamic_call("void sys_big_data (const char*, size_t)",
        [] (Script& script) {
            const auto [buffer] = script.args <riscv::Buffer> ();
            // OR
            const auto [view] = script.args <std::string_view> ();
        });
  11. Verify public functions in a micro binary

    master

    To check if a specific function is exported and available in a compiled binary, use the readelf tool from the RISC-V toolchain:

    riscv32-unknown-elf-readelf -a <mybinary>

    In the resulting symbol table, look for the function name. It should be marked as FUNC. If it is marked as UND (undefined), the function is missing from the binary.

  12. Manage timers with the Timers class

    master

    The Timers class provides an interface for scheduling one-shot and periodic tasks. Timers are identified by an integer ID returned upon creation. You must call handle_events() periodically to process and execute the scheduled callbacks.

    Key operations:

    • oneshot(when, handler): Schedules a single execution at a specific time.
    • periodic(when, period, handler): Schedules a task to run at when, and then repeatedly every period.
    • stop(id): Cancels a scheduled timer.
    • set_repeats(id, repeats): Updates how many times a periodic timer should repeat.
    • active(): Returns the number of currently active timers.
    • next(): Returns the time remaining until the next timer event.
    // Example usage of the Timers API
    Timers timer_manager;
    
    // Schedule a one-shot event in 5.0 units of time
    timer_manager.oneshot(5.0, [](int id) {
        // Handle event
    });
    
    // Schedule a periodic event starting at 1.0, repeating every 0.5
    timer_manager.periodic(1.0, 0.5, [](int id) {
        // Handle periodic event
    });
    
    // Main loop to process timer events
    while (true) {
        timer_manager.handle_events();
    }