Kernal Lua API

Complete reference for the kernal global table available to all scripts. Build overlays, read and write process memory, handle input, and automate game analysis — all from Lua.

All kernal.* functions are synchronous from your script's perspective. Memory reads, process operations, and overlay commands are internally asynchronous but automatically awaited — you write normal sequential code and it just works.

Logging & Debug

Three log levels let you categorize output in the script console. All accept variadic arguments which are concatenated with spaces, similar to print() in standard Lua.

kernal.log(...)

Print an info-level message. Use for general status updates and data inspection.

kernal.log("Hello", "world", 123)
-- Output: Hello world 123

-- Useful for debugging values:
kernal.log("Player HP:", hp, "Team:", team)

kernal.warn(...)

Print a warning. Appears highlighted in the console. Use for non-fatal issues.

kernal.warn("Health is low:", hp)
kernal.warn("Entity pointer is NULL, skipping")

kernal.error(...)

Print an error message. Use for failures that don't necessarily crash the script.

kernal.error("Failed to read memory at", addr)
kernal.error("Module not found: engine2.dll")

Timing

Control script execution speed and measure performance. Every main loop should include a sleep() or sync() call to prevent 100% CPU usage.

kernal.sleep(ms)

Pause execution for the specified number of milliseconds. This yields the thread, allowing the engine to process other tasks.

ParamTypeDescription
msnumberMilliseconds to sleep (0 = yield without delay)
kernal.sleep(16)  -- ~60 FPS tick rate
kernal.sleep(1)   -- maximum speed (~1000 FPS)
kernal.sleep(100) -- 10 updates/sec (low CPU usage)

kernal.now()

Returns the current timestamp in milliseconds (Unix epoch). Use for measuring elapsed time between operations.

Returns: number
local start = kernal.now()
-- ... perform expensive work ...
local elapsed = kernal.now() - start
kernal.log("Frame took " .. elapsed .. "ms")

-- Example: refresh entity list every 5 seconds
if kernal.now() - lastRefresh > 5000 then
    entity_list = kernal.read_i64(base + offsets.dwEntityList)
    lastRefresh = kernal.now()
end

kernal.time_ms() NEW

Returns a high-resolution monotonic timestamp in milliseconds. Ideal for frame timing and delta calculations — not affected by clock drift.

Returns: number (ms)
local frameStart = kernal.time_ms()
-- ... frame work ...
local elapsed = kernal.time_ms() - frameStart
local remaining = 6.9 - elapsed -- cap to ~144 FPS
if remaining > 0 then kernal.sleep(remaining) end

Process Management

Before reading or writing memory, you must attach to a target process. Kernal supports attaching by process name (searches for a running process) or by PID directly.

kernal.attach(pidOrName)

Attach to a process. This opens a handle and prepares memory access. Must be called before any read_* or write_* functions.

ParamTypeDescription
pidOrNamenumber | stringProcess ID or executable name (e.g. "game.exe")
Returns: number (PID of attached process)
-- Attach by name (most common)
kernal.attach("game.exe")

-- Attach by PID (useful if multiple instances)
kernal.attach(12345)

-- Typical startup pattern:
kernal.log("Waiting for game...")
kernal.attach("game.exe")
kernal.log("Attached to PID: " .. kernal.get_pid())

kernal.detach()

Detach from the current process. Called automatically when your script ends, but you can call it manually to cleanly release the handle.

kernal.detach()
kernal.log("Disconnected")

kernal.process_list()

Returns a list of all running processes on the system. Each entry has pid, name, and exe fields.

Returns: table of {pid, name, exe}
local procs = kernal.process_list()
for _, p in ipairs(procs) do
    if p.name == "game.exe" then
        kernal.log("Found at PID " .. p.pid)
        break
    end
end
-- Launcher detection example:
local launcher = "unknown"
local ok, procs = pcall(kernal.process_list)
if ok and procs then
    for _, p in ipairs(procs) do
        local name = (type(p) == "table" and p.name or tostring(p)):lower()
        if name:find("eadesktop") or name:find("origin") then
            launcher = "EA"; break
        elseif name:find("steam") then
            launcher = "Steam"; break
        end
    end
end
kernal.log("Launcher: " .. launcher)

kernal.is_attached() NEW

Check if the script is currently attached to a process. Useful for connection-state checks in your main loop.

Returns: boolean
if not kernal.is_attached() then
    kernal.error("Lost connection to process!")
    return
end

kernal.get_pid() NEW

Get the PID of the currently attached process. Returns 0 if not attached.

Returns: number
kernal.log("Working with PID: " .. kernal.get_pid())

Memory — Reading

Read values from the attached process's memory space. All read functions require kernal.attach() to be called first. If the address is invalid or the process has exited, these functions will throw an error (catch with pcall).

kernal.read(addr, len)

Read raw bytes and return them as a hex-encoded string. Useful for dumping unknown memory regions.

ParamTypeDescription
addrnumberMemory address to read from
lennumberNumber of bytes to read
Returns: string (hex-encoded)
local hex = kernal.read(base + 0x100, 16)
kernal.log(hex) -- "48894c240848895424105355..."

kernal.read_i32(addr)

Read a signed 32-bit integer (4 bytes). Most common for health, ammo, team numbers, etc.

Returns: number
local health = kernal.read_i32(pawn + 0x34C)
local team = kernal.read_i32(pawn + 0x3EB)
kernal.log("HP:", health, "Team:", team)

kernal.read_u32(addr)

Read an unsigned 32-bit integer. Used for handles, flags, and bitmasks that shouldn't be sign-extended.

Returns: number
local handle = kernal.read_u32(controller + 0x90C)
if handle == 0xFFFFFFFF then
    kernal.warn("Invalid handle")
end

kernal.read_i64(addr)

Read a signed 64-bit integer. Essential for reading pointers on 64-bit processes. Returned as a Lua number (safe up to 2^53).

Returns: number
-- Read a pointer (64-bit address)
local entityList = kernal.read_i64(base + 0x24D0DC0)
kernal.log(string.format("Entity list @ 0x%X", entityList))

kernal.read_f32(addr)

Read a 32-bit floating-point value. Used for positions, velocities, angles, and any decimal game data.

Returns: number
-- Read player position (vec3 = 3 consecutive floats)
local x = kernal.read_f32(pawn + 0x1390)
local y = kernal.read_f32(pawn + 0x1394)
local z = kernal.read_f32(pawn + 0x1398)
kernal.log(string.format("Pos: %.1f, %.1f, %.1f", x, y, z))

Buffers & Batching

Individual memory reads are fast (~1–2 ms each), but in a main loop reading 50+ values per frame, that latency adds up. Buffers solve this by reading large chunks in a single IPC call, then letting you extract values locally with zero additional cost.

kernal.read_buf(addr, len)

Read a contiguous block of memory into a buffer object. This is the primary optimization tool — use it whenever you need multiple values from the same memory region.

ParamTypeDescription
addrnumberStart address
lennumberNumber of bytes to read
Returns: Buffer object

Buffer methods:

MethodDescription
.i32(offset)Signed 32-bit int at byte offset
.u32(offset)Unsigned 32-bit int at byte offset
.i64(offset)Signed 64-bit int at byte offset
.f32(offset)32-bit float at byte offset
.str(offset, maxLen)Null-terminated UTF-8 string starting at offset (default maxLen=256)
.len()Total buffer size in bytes
-- Read a 4x4 view matrix (16 floats = 64 bytes) in ONE call
local buf = kernal.read_buf(base + 0x2330AE0, 64)
local vm = {}
for i = 0, 15 do
    vm[i] = buf.f32(i * 4)  -- extract each float at its byte offset
end

-- Read player position (3 floats = 12 bytes)
local pos = kernal.read_buf(pawn + 0x1390, 12)
local x, y, z = pos.f32(0), pos.f32(4), pos.f32(8)

kernal.read_multi(addr1, len1, addr2, len2, ...)

Scatter-gather read — reads multiple non-contiguous memory regions in a single IPC round-trip. This is the fastest way to read data from many different addresses at once.

Pass flat pairs of (address, length). Results are packed sequentially in the returned buffer: region 1 starts at offset 0, region 2 at offset len1, region 3 at offset len1+len2, etc.

ParamTypeDescription
addr1numberFirst address to read
len1numberBytes to read from addr1
......More (address, length) pairs
Returns: Buffer object
-- Read view matrix (64B) + local pawn pointer (8B) in ONE call
local buf = kernal.read_multi(
    base + 0x2330AE0, 64,  -- view matrix at offset 0
    base + 0x2056700, 8    -- local pawn at offset 64
)
local m00 = buf.f32(0)       -- first matrix element
local pawn = buf.i64(64)    -- pawn pointer
-- Dynamic: batch-read health from N players
local args = {}
for _, pawn in ipairs(pawnAddrs) do
    args[#args + 1] = pawn + 0x34C  -- health offset
    args[#args + 1] = 4              -- 4 bytes (i32)
end
local buf = kernal.read_multi(table.unpack(args))

for i = 1, #pawnAddrs do
    local hp = buf.i32((i - 1) * 4)
    kernal.log("Player " .. i .. " HP: " .. hp)
end

Memory — Writing

Write values into the target process's memory. Use with caution — writing incorrect values can crash the target. All write functions require an active attach().

kernal.write(addr, hex)

Write raw bytes from a hex string. Commonly used for patching instructions (e.g. NOPs).

ParamTypeDescription
addrnumberTarget address
hexstringHex-encoded bytes
-- NOP out a 4-byte instruction
kernal.write(addr, "90909090")

-- Patch a JNE to JMP (force branch)
kernal.write(jumpAddr, "EB")

kernal.write_i32(addr, value)

Write a signed 32-bit integer.

kernal.write_i32(pawn + 0x34C, 100) -- set health to 100

kernal.write_f32(addr, value) NEW

Write a 32-bit floating-point value.

ParamTypeDescription
addrnumberTarget address
valuenumberFloat value to write
kernal.write_f32(pawn + 0x1390, 0.0)   -- zero out X position
kernal.write_f32(viewAngle, 90.0)       -- set view angle

kernal.write_i64(addr, value) NEW

Write a 64-bit integer. Useful for writing pointers or large values.

ParamTypeDescription
addrnumberTarget address
valuenumber64-bit integer value
kernal.write_i64(pointerSlot, newAddress)

Memory — Quality of Life

Convenience wrappers that simplify common memory reading patterns. These save you from manual offset math and type conversions.

kernal.read_string(addr, maxLen) NEW

Read a null-terminated UTF-8 string from memory.

ParamTypeDefaultDescription
addrnumberMemory address
maxLennumber256Maximum bytes to read (capped at 4096)
Returns: string
local name = kernal.read_string(nameAddr, 64)
kernal.log("Player name: " .. name)

kernal.read_vec3(addr) NEW

Read 3 consecutive floats (12 bytes) as a vector. Shorthand for reading X, Y, Z coordinates.

Returns: table with x, y, z fields
local pos = kernal.read_vec3(pawn + 0x1390)
kernal.log(string.format("Pos: %.1f, %.1f, %.1f", pos.x, pos.y, pos.z))

kernal.read_bool(addr) NEW

Read a single byte and return as boolean.

Returns: boolean
local isDormant = kernal.read_bool(pawn + 0xEF)

kernal.read_u64(addr) NEW

Read an unsigned 64-bit integer (safe up to 2^53).

Returns: number
local ptr = kernal.read_u64(base + 0x100)

kernal.is_valid_ptr(addr) NEW

Check if an address points to a valid mapped memory region. Uses VirtualQueryEx — faster than wrapping every read in pcall.

Returns: boolean
if kernal.is_valid_ptr(pawnAddr) then
    local hp = kernal.read_i32(pawnAddr + 0x34C)
end

Pointer Chains & Batch Operations

Advanced functions that reduce IPC round-trips by performing multi-step operations server-side. These are critical for high-performance scripts that follow pointer chains or write to multiple locations.

kernal.read_chain(addr, offsets, finalSize) NEW

Follow a pointer chain server-side in one IPC call. Eliminates multi-step pointer resolution. At each step: read 8 bytes (pointer), add the next offset. After all offsets, reads finalSize bytes at the final address.

ParamTypeDescription
addrnumberStarting address
offsetstableList of offsets {off1, off2, ...}
finalSizenumberBytes to read at the resolved final address
Returns: Buffer (same as read_buf) + .addr() for the resolved final address
-- Before: 3 separate IPC calls
local entryPtr = kernal.read_i64(entityList + 0x10)
local pawnChunk = kernal.read_i64(entryPtr + chunkIdx * 8)
local pawn = kernal.read_i64(pawnChunk + slotIdx * 0x70)

-- After: 1 IPC call
local buf = kernal.read_chain(entityList, {0x10, chunkIdx * 8, slotIdx * 0x70}, 8)
local pawn = buf.i64(0)

kernal.write_multi(addr1, hex1, addr2, hex2, ...) NEW

Batch-write multiple memory locations in one IPC call.

ParamTypeDescription
addr1numberFirst address
hex1stringHex bytes for first write
......More addr/hex pairs
Returns: {written: count}
kernal.write_multi(
    addr1, "90909090",
    addr2, "01000000"
)

kernal.module_base_multi(name1, name2, ...) NEW

Resolve multiple module bases in one call. Enumerates modules only once.

Returns: table (0-indexed) of {base, size} or null for each name
local mods = kernal.module_base_multi("client.dll", "engine2.dll", "schemasystem.dll")
local clientBase = mods[0].base
local engineBase = mods[1].base

Modules

Get information about loaded DLLs and modules in the target process. Module base addresses are the foundation for offset-based memory access — all game structures live at moduleBase + offset.

kernal.module_base(name)

Get the base address of a loaded module. This is the starting address where the DLL is mapped in memory.

ParamTypeDescription
namestringModule filename (e.g. "client.dll", "engine2.dll")
Returns: number (base address)
local base = kernal.module_base("client.dll")
kernal.log(string.format("client.dll @ 0x%X", base))

-- Now you can read game data using base + offset:
local entityList = kernal.read_i64(base + 0x24D0DC0)

kernal.module_size()

Get the size (in bytes) of the last module looked up with module_base(). Useful for bounds checking or scanning.

Returns: number (bytes)
local base = kernal.module_base("client.dll")
local size = kernal.module_size()
kernal.log(string.format("client.dll: 0x%X — %d MB", base, size / 1024 / 1024))

Overlay — Lifecycle

The overlay is a transparent, always-on-top, click-through window that renders over the game. You control it with a simple open/begin/draw/end/close pattern.

kernal.overlay_open()

Create and display the overlay window. Call once at script startup before entering your main loop.

kernal.overlay_open()
kernal.log("Overlay active")

kernal.overlay_begin()

Begin a new frame. Clears all draw commands from the previous frame. Call at the top of each loop iteration before any draw_* calls.

kernal.overlay_end()

Flush all draw commands and render the frame to screen. Call at the end of each loop iteration after all draw_* calls.

Note: This is fire-and-forget — it queues the frame for rendering but doesn't wait for it to be painted. On high-FPS hardware (300+), the script can outrun the renderer, causing increasing overlay latency. Use flush() or sync() after this call to prevent latency buildup.

kernal.overlay_close()

Destroy the overlay window. Called automatically when your script stops, but you can call it manually.

kernal.flush() NEW

Block the script until the renderer confirms it painted the current frame. Prevents the script from outrunning the overlay — if hardware can't keep up, FPS drops but latency stays near 0 ms. Call after overlay_end().

kernal.overlay_end()
kernal.flush()  -- script waits here until the frame is on screen

kernal.sync() NEW

Convenience: flush() + yield to event loop. Equivalent to flush() then sleep(0). Use this as your frame-end call for zero-latency scripts that don't need a fixed FPS cap.

kernal.overlay_end()
kernal.sync()  -- flush + yield, loop runs as fast as renderer can paint

kernal.overlay_set_clickthrough(enabled) NEW

Toggle click-through mode on the overlay. When false, the overlay receives mouse clicks (for interactive menus). When true (default), clicks pass through to the game.

ParamTypeDescription
enabledbooleantrue = click-through, false = interactive
kernal.overlay_set_clickthrough(false) -- overlay can now receive clicks
-- ... interactive menu logic ...
kernal.overlay_set_clickthrough(true)  -- back to click-through

kernal.set_draw_fps(fps) NEW

Set the overlay draw FPS cap independently from script tick rate. Scripts continue running at full speed (e.g. aimbot at 500 Hz) while overlay redraws are capped (e.g. 144 Hz). Pass 0 to disable the cap.

ParamTypeDescription
fpsnumberTarget draw FPS (0 = uncapped)
kernal.set_draw_fps(144)  -- overlay at 144 Hz, script ticks unlimited
-- Standard overlay loop pattern:
kernal.overlay_open()

while true do
    kernal.overlay_begin()

    -- ... your draw calls here ...
    kernal.draw_text("Hello!", 10, 10, "#FFFFFF", 16)

    kernal.overlay_end()
    kernal.sync()  -- zero-latency frame pacing
end

kernal.overlay_close()

Overlay — Drawing Primitives

All draw functions must be called between overlay_begin() and overlay_end(). Colors are CSS-format hex strings (e.g. "#FF0000" for red). Coordinates are in screen pixels (0,0 = top-left).

kernal.draw_rect(x, y, w, h, color, thickness)

Draw a rectangle outline. The bread-and-butter for bounding boxes.

ParamTypeDefaultDescription
x, ynumberTop-left corner
w, hnumberWidth and height
colorstring"#FF0000"Outline color
thicknessnumber2Line width in pixels
-- Enemy bounding box
kernal.draw_rect(screenX - w/2, screenY, w, h, "#FF4444", 2)

kernal.draw_rect_fill(x, y, w, h, color)

Draw a filled rectangle. Great for health bars, backgrounds, and indicators.

-- Health bar with background
kernal.draw_rect_fill(10, 10, 200, 20, "#333333")           -- bg
kernal.draw_rect_fill(10, 10, 200 * (hp/100), 20, "#44FF44") -- fill

kernal.draw_rect_rounded(x, y, w, h, radius, color, thickness) NEW

Draw a rounded rectangle outline. Great for modern-looking menu panels.

ParamTypeDefaultDescription
x, ynumberTop-left corner
w, hnumberWidth and height
radiusnumber5Corner radius
colorstring"#FF0000"Outline color
thicknessnumber2Line width
kernal.draw_rect_rounded(100, 100, 200, 150, 10, "#FFFFFF", 2)

kernal.draw_line(x1, y1, x2, y2, color, thickness)

Draw a line between two points. Used for snaplines (tracers from screen edge to entities).

ParamTypeDefaultDescription
x1, y1numberStart point
x2, y2numberEnd point
colorstring"#FF0000"Line color
thicknessnumber2Line width
-- Snapline from bottom-center of screen to player
local SW = kernal.screen_width()
local SH = kernal.screen_height()
kernal.draw_line(SW/2, SH, playerX, playerY, "#FFFF00", 1)

kernal.draw_circle(x, y, r, color, thickness)

Draw a circle outline.

ParamTypeDefaultDescription
x, ynumberCenter point
rnumberRadius in pixels
colorstring"#FF0000"Circle color
thicknessnumber2Line width
-- Crosshair circle
kernal.draw_circle(SW/2, SH/2, 5, "#00FFFF", 1)

kernal.draw_circle_fill(x, y, r, color) NEW

Draw a filled circle. Ideal for head dots, points of interest, or indicators.

-- Head dot on enemy
kernal.draw_circle_fill(headX, headY, 4, "#FF0000")

-- Indicator dot
kernal.draw_circle_fill(20, 20, 6, "#44FF44")

kernal.draw_text(text, x, y, color, size)

Render text on the overlay. Use for labels, status info, and debug data.

ParamTypeDefaultDescription
textstringText to display
x, ynumberPosition (top-left of text)
colorstring"#FFFFFF"Text color
sizenumber14Font size in pixels
-- HUD title
kernal.draw_text("KERNAL ESP", 10, 20, "#00FF00", 18)

-- Player name above box
kernal.draw_text(playerName, boxX, boxY - 14, "#FFFFFF", 11)

-- Distance label
kernal.draw_text(math.floor(dist) .. "m", boxX, boxY + boxH + 2, "#AAAAAA", 10)

kernal.draw_text_centered(text, x, y, color, size) NEW

Draw center-aligned text. The x coordinate is the center point, not the left edge.

kernal.draw_text_centered("MENU TITLE", SW/2, 50, "#FFFFFF", 24)

kernal.text_width(text, size) NEW

Returns the approximate pixel width of a string at the given font size. Useful for menu layout math.

Returns: number (pixel width)
local w = kernal.text_width("Hello World", 14)

kernal.draw_progress_bar(x, y, w, h, fraction, fgColor, bgColor) NEW

Draw a progress/health bar with background and foreground fill.

ParamTypeDefaultDescription
x, ynumberTop-left corner
w, hnumberFull bar dimensions
fractionnumber0Fill amount (0.0 to 1.0)
fgColorstring"#00FF00"Foreground (fill) color
bgColorstring"#333333"Background color
kernal.draw_progress_bar(100, 200, 200, 20, hp / 100, "#44FF44", "#333333")

kernal.draw_image(path, x, y, w, h) NEW

Render a PNG or image file on the overlay.

ParamTypeDescription
pathstringFile path or URL to image
x, ynumberTop-left corner
w, hnumberRender dimensions
kernal.draw_image("C:/path/to/logo.png", 10, 10, 64, 64)

Input — Keyboard & Keys

Check keyboard and mouse button states for toggles, hotkeys, and interactive scripts. All key checks are non-blocking and return the current state instantly.

kernal.key_down(key) NEW

Check if a key is currently held down. Supports key names and virtual key codes.

ParamTypeDescription
keystring | numberKey name ("F1", "SHIFT", "MOUSE4") or VK code
Returns: boolean

Supported key names: AZ, 09, F1F12, SHIFT, CTRL, ALT, SPACE, ENTER, ESC, TAB, MOUSE4, MOUSE5, LBUTTON, RBUTTON, MBUTTON, UP, DOWN, LEFT, RIGHT, INSERT, DELETE, HOME, END, PAGEUP, PAGEDOWN, CAPSLOCK, NUMPAD0NUMPAD9, LSHIFT, RSHIFT, LCONTROL, RCONTROL, LALT, RALT

if kernal.key_down("SHIFT") then
    kernal.log("Shift is held!")
end

kernal.key_pressed(key) NEW

Edge-triggered key check — returns true once on the frame the key was pressed, not while held.

Returns: boolean
if kernal.key_pressed("F1") then
    espEnabled = not espEnabled
end

kernal.mouse_pos() NEW

Get current cursor position on screen.

Returns: table with x, y
local mp = kernal.mouse_pos()
kernal.log("Cursor at: " .. mp.x .. ", " .. mp.y)

kernal.key_states(...) NEW

Batch key check — queries multiple keys in a single IPC call. Use this when you need to check 3+ keys per frame to avoid sequential round-trips.

ParamTypeDescription
...string | numberKey names or VK codes (variadic)
Returns: table (0-indexed) of {down, pressed} for each key
local keys = kernal.key_states("LBUTTON", "SHIFT", "F1")
if keys[0].down then   -- fire held
    -- aimbot logic
end
if keys[2].pressed then -- F1 toggled
    espEnabled = not espEnabled
end

kernal.read_inputs(keys) NEW

Combined input read — key states and mouse cursor in one IPC call. Ideal for the top of your game loop to gather all input state at once.

ParamTypeDescription
keystableList of key names or VK codes

Returns: Flat table (avoids nested-object issues):

FieldTypeDescription
k0_dbooleanKey 0 down (held)
k0_pbooleanKey 0 pressed (edge)
k1_d, k1_pbooleanKey 1 states
mxnumberMouse X
mynumberMouse Y
nnumberNumber of keys queried
local inp = kernal.read_inputs({"LBUTTON", "SHIFT", "F1"})
local fire = inp.k0_d     -- LBUTTON held
local shift = inp.k1_d    -- SHIFT held
local toggle = inp.k2_p   -- F1 edge-triggered
local mx, my = inp.mx, inp.my

Mouse & Aimbot

Functions for programmatic mouse control. Used for recoil compensation, aimbot smoothing, and automated clicking.

kernal.mouse_move(dx, dy) NEW

Relative mouse movement via SendInput. Bypasses game raw input hooks — ideal for RCS and aimbot smoothing.

ParamTypeDescription
dxnumberHorizontal pixels to move
dynumberVertical pixels to move
-- Simple RCS: compensate recoil
kernal.mouse_move(0, -2)

kernal.mouse_move_to(x, y) NEW

Move cursor to absolute screen position.

ParamTypeDescription
xnumberScreen X coordinate
ynumberScreen Y coordinate
kernal.mouse_move_to(SW/2, SH/2) -- center of screen

kernal.mouse_click(button) NEW

Simulate a mouse button click (down + up).

ParamTypeDefaultDescription
buttonstring"left""left", "right", or "middle"
kernal.mouse_click("left")

Script Lifecycle

Control how your script starts, stops, and manages frame timing. These functions help you write clean, well-behaved scripts that release resources properly.

kernal.on_stop(fn) NEW

Register a cleanup callback that runs when the script is stopped (by the user or by an error). Guaranteed to fire — use it for overlay_close, detach, etc.

kernal.on_stop(function()
    kernal.overlay_close()
    kernal.detach()
    kernal.log("Cleaned up!")
end)

kernal.set_fps(n) NEW

Set a target frame rate. Use with delta_time() for frame-rate-independent logic. This sets internal state — you still need sleep() in your loop.

ParamTypeDescription
nnumberTarget FPS (0 = unlimited)
kernal.set_fps(144) -- target 144 FPS

kernal.delta_time() NEW

Returns milliseconds since the last call to delta_time(). Useful for smooth interpolation and frame-rate-independent logic.

Returns: number (ms)
local dt = kernal.delta_time()
local smoothFactor = dt / 16.0 -- normalize to ~60 FPS

Display Info

Query the physical screen resolution. These values account for DPI scaling and return the actual pixel dimensions your overlay renders at.

kernal.screen_width()

Returns: number (pixels)

kernal.screen_height()

Returns: number (pixels)
local SW = kernal.screen_width()   -- e.g. 2560
local SH = kernal.screen_height()  -- e.g. 1440
kernal.log(string.format("Resolution: %dx%d", SW, SH))

-- Center of screen (useful for crosshairs, snaplines)
local cx, cy = SW/2, SH/2

Offsets

Game offsets are memory addresses for specific structures (entity lists, view matrices, player data). They change with every game update and must be kept current for your scripts to work.

Where to find offsets:

kernal.get_offset(game, key)

Retrieve a named offset from the injected offset table (populated by the Kernal UI when launching scripts — future feature). Currently returns nil unless offsets are passed by the launcher.

ParamTypeDescription
gamestringGame identifier
keystringOffset name
Returns: number or nil
-- Recommended: hardcode offsets at the top of your script
-- Update these when the game patches
local ENTITY_LIST   = 0x24D0DC0
local LOCAL_PLAYER  = 0x2056700
local VIEW_MATRIX   = 0x2330AE0
local HEALTH        = 0x34C
local TEAM          = 0x3EB
local POSITION      = 0x1390

Latency & Flow Control

The overlay pipeline uses ack-based flow control to prevent latency buildup. Understanding this is critical for writing smooth, low-latency scripts.

The Problem

When a script runs faster than the overlay renderer can paint (common on 300+ FPS hardware), draw commands queue up in Chromium's IPC buffer. The overlay renders frames in order — including stale ones from seconds ago — creating a growing delay between game state and what's drawn on screen.

The Solution

The main process only sends a new frame to the renderer after the renderer acknowledges the previous one. At most 2 frames exist at any time: 1 being painted + 1 pending (latest-only). Older pending frames are silently replaced.

Usage Patterns

Pattern 1: Zero-latency ESP (recommended for most scripts)

for tick = 1, 999999 do
    -- ... read game data ...
    kernal.overlay_begin()
    -- ... draw commands ...
    kernal.overlay_end()
    kernal.sync()  -- waits for paint, then yields — latency stays ~0 ms
end

Pattern 2: Decoupled aimbot + overlay

kernal.set_draw_fps(144)  -- overlay capped at 144 Hz
for tick = 1, 999999 do
    -- ... read game data ...
    kernal.overlay_begin()
    -- ... draw commands ...
    kernal.overlay_end()  -- skipped if < 1/144s since last drawn frame
    -- ... aimbot logic (runs every tick, not capped) ...
    kernal.sleep(0)  -- tick as fast as possible for aimbot
end

Pattern 3: Frame-locked rendering

kernal.overlay_end()
kernal.flush()  -- block until THIS frame is painted
-- guaranteed: next read_multi gets data from AFTER the frame was displayed

Safety

If the renderer crashes or freezes, a 100 ms safety timeout automatically clears the ack gate so the script doesn't hang permanently.

Performance Tips

Each kernal.* memory call is an IPC round-trip to the helper process (~1–2 ms). At 50 calls per frame, that's 50–100 ms of latency — making your overlay feel sluggish. Here's how to stay fast:

  1. Use read_multi() for everything — batch ALL per-frame reads (HP, team, position, view matrix) into ONE call. This is the single biggest optimization. 1 IPC ≈ 1.5 ms. 7 IPC ≈ 10 ms.
  2. Read view matrix LAST — put it at the end of your read_multi so the helper reads it at the very end. This gives you the freshest camera data.
  3. Cache entity resolution — controller → pawn pointer lookups rarely change (only when players join or leave). Resolve them every 25–100 frames, not every frame.
  4. Use sync() after overlay_end() for zero-latency rendering — the script waits for the renderer to finish painting before the next tick. If the renderer can't keep up, FPS drops but latency stays ~0 ms.
  5. Use sleep(0) only if you don't care about overlay latency (e.g. pure aimbot with no ESP). Use sleep(16) to save CPU.
  6. Use set_draw_fps() to decouple aimbot from overlay — run your aimbot at 500 Hz while the overlay only redraws at 144 Hz.
  7. Use read_buf() for contiguous data — read a whole struct (health, team, position) in one call instead of 3 separate reads.
  8. Wrap player reads in pcall() — players disconnect mid-frame and their memory becomes invalid. pcall catches the error instead of crashing your script.
  9. Use read_inputs() to get key states + mouse position in a single call — collapses 3–5 calls into 1.
  10. Use on_stop() for cleanup — guarantees overlay_close() and detach() run even on errors.
  11. Binary pipe — memory reads automatically use a high-speed binary channel when available (v0.12+), eliminating hex encode/decode overhead.
  12. Don't log every framekernal.log in a 500 Hz loop floods the UI. Log only on state changes or every N frames.
  13. Flush vs. Syncflush() blocks until the current frame is painted (use for frame-locked scripts). sync() does flush() + yield (use as your main loop frame-end). Both prevent the 5+ second latency that occurs when scripts outrun the overlay renderer.

Error Handling

Memory reads throw Lua errors when the address is invalid, the process has exited, or you're not attached. Use pcall to handle these gracefully without crashing your script.

-- Safe read with error handling
local ok, val = pcall(function()
    return kernal.read_i32(addr)
end)

if ok then
    kernal.log("Value: " .. val)
else
    kernal.warn("Read failed: " .. tostring(val))
end
-- Common pattern: wrap each entity in pcall
-- so one bad pointer doesn't kill the whole loop
for i = 0, 63 do
    pcall(function()
        local entity = getEntity(i)
        if entity == 0 then return end
        -- ... read and draw ...
    end)
end

API Version

Query the current API version supported by the running Kernal instance:

kernal.log("API version: " .. kernal.version) -- "0.3"

Complete Example — CS2 ESP

A fully working external ESP overlay with enemy boxes, health bars, and HP text. Uses entity caching and a mega read_multi for 1 IPC call per frame on normal ticks. Targets CS2 but the pattern applies to any game.

Key techniques:

-- CS2 Simple Enemy ESP (optimized — 1 IPC/frame, zero-latency overlay)

-- Offsets (update after game patches)
local dwEntityList      = 0x24D0DC0
local dwLocalPlayerPawn = 0x2056700
local dwViewMatrix      = 0x2330AE0
local m_iHealth         = 0x34C
local m_iTeamNum        = 0x3EB
local m_vOldOrigin      = 0x1390
local m_hPlayerPawn     = 0x90C
local ENT_STRIDE        = 0x70
local CACHE_EVERY       = 25

-- Setup
kernal.attach("cs2.exe")
local base = kernal.module_base("client.dll")
local SW = kernal.screen_width()
local SH = kernal.screen_height()
local SW2, SH2 = SW/2, SH/2

kernal.overlay_open()
kernal.on_stop(function()
    kernal.overlay_close()
    kernal.detach()
end)

-- Entity list pointers (stable, refresh periodically)
local entity_list = kernal.read_i64(base + dwEntityList)
local list_entry  = kernal.read_i64(entity_list + 0x10)

-- Entity cache: resolved pawn addresses
local cachedPawns = {}
local lastLocalPawn = 0

-- World to screen projection
local function w2s(px, py, pz, vm)
    local w = vm[12]*px + vm[13]*py + vm[14]*pz + vm[15]
    if w < 0.01 then return nil, nil end
    return SW2 + (vm[0]*px + vm[1]*py + vm[2]*pz + vm[3]) / w * SW2,
           SH2 - (vm[4]*px + vm[5]*py + vm[6]*pz + vm[7]) / w * SH2
end

-- Main loop
for tick = 1, 999999 do
    -- Refresh cache every N frames (entity resolution = extra IPC)
    if tick % CACHE_EVERY == 1 or #cachedPawns == 0 then
        -- ... resolve controller → pawn pointers ...
        -- (batch read handles, resolve chunks, cache valid pawns)
    end

    -- ONE IPC: all player data + localTeam + view matrix
    local megaA = {}
    megaA[1] = lastLocalPawn + m_iTeamNum; megaA[2] = 4
    for _, pawn in ipairs(cachedPawns) do
        megaA[#megaA+1] = pawn + m_iHealth;    megaA[#megaA+1] = 4
        megaA[#megaA+1] = pawn + m_iTeamNum;   megaA[#megaA+1] = 4
        megaA[#megaA+1] = pawn + m_vOldOrigin;  megaA[#megaA+1] = 12
    end
    -- View matrix LAST for maximum freshness
    megaA[#megaA+1] = base + dwViewMatrix; megaA[#megaA+1] = 64

    local ok, buf = pcall(kernal.read_multi, table.unpack(megaA))
    if not ok or not buf then
        kernal.overlay_begin(); kernal.overlay_end()
        kernal.sync(); goto continue
    end

    -- Parse view matrix from end of buffer
    local vmOff = 4 + #cachedPawns * 20
    local vm = {}
    for i = 0, 15 do vm[i] = buf.f32(vmOff + i*4) end

    -- Draw
    kernal.overlay_begin()
    local localTeam = buf.i32(0)

    for n, pawn in ipairs(cachedPawns) do
        pcall(function()
            local off = 4 + (n-1) * 20
            local hp   = buf.i32(off)
            local team = buf.i32(off + 4)
            local px   = buf.f32(off + 8)
            local py   = buf.f32(off + 12)
            local pz   = buf.f32(off + 16)

            if team == localTeam then return end
            if hp <= 0 or hp > 100 then return end

            local sfx, sfy = w2s(px, py, pz, vm)
            local shx, shy = w2s(px, py, pz + 72, vm)
            if not (sfx and shx) then return end

            local boxH = sfy - shy
            if boxH < 5 then return end
            local boxW = boxH / 2.4

            kernal.draw_rect(shx - boxW/2, shy, boxW, boxH, "#FF4444", 2)

            local hpFrac = hp / 100
            local barH   = boxH * hpFrac
            local hpColor = "#44FF44"
            if     hpFrac <= 0.25 then hpColor = "#FF4444"
            elseif hpFrac <= 0.5  then hpColor = "#FFAA00" end
            kernal.draw_rect_fill(shx - boxW/2 - 6, sfy - barH, 3, barH, hpColor)
            kernal.draw_text(hp .. " HP", shx - boxW/2, shy - 14, "#FFFFFF", 11)
        end)
    end

    kernal.overlay_end()
    kernal.sync()  -- wait for renderer to paint — zero latency
    ::continue::
end

Complete Example — Apex Legends ESP

A minimal, high-performance external ESP for Apex Legends. Box + health bar + shield bar only. Uses split scanning (fast player scan every frame, slow bot scan in background) and a single read_multi mega-read for all entity data.

Key techniques:

-- Apex Legends ESP — box + health + shield (zero-latency overlay)

-- Offsets (update after game patches)
local OFF = {
    cl_entitylist   = 0x6266928,
    local_player    = 0x268FA08,
    view_render     = 0x3D97D78,
    view_matrix_off = 0x11A350,
    m_iHealth       = 0x0324,
    m_iMaxHealth    = 0x0468,
    m_iTeamNum      = 0x0334,
    m_vecAbsOrigin  = 0x017C,
    m_lifeState     = 0x0690,
    m_bleedoutState = 0x27E0,
    m_shieldHealth  = 0x01A0,
    m_shieldHealthMax = 0x01A4,
}

-- Setup
kernal.attach("r5apex_dx12.exe")
local base = kernal.module_base("r5apex_dx12.exe")
local SW = kernal.screen_width()
local SH = kernal.screen_height()
local SW2, SH2 = SW / 2, SH / 2

kernal.overlay_open()
kernal.on_stop(function()
    kernal.overlay_close()
    kernal.detach()
end)

-- Fast player scan (slots 0-127, every frame)
local function scanPlayers(localPlayer)
    local ptrBuf = kernal.read_buf(base + OFF.cl_entitylist, 128 * 32)
    local ptrs, count = {}, 0
    for i = 0, 127 do
        local entPtr = ptrBuf.i64(i * 32)
        if entPtr ~= 0 and entPtr ~= localPlayer then
            count = count + 1
            ptrs[count] = entPtr
        end
    end
    return ptrs, count
end

-- World to screen
local vm = {[0]=0,0,0,0, 0,0,0,0, 0,0,0,0, 0,0,0,0}
local function w2s(px, py, pz)
    local w = vm[12]*px + vm[13]*py + vm[14]*pz + vm[15]
    if w < 0.01 then return nil, nil end
    local invW = 1.0 / w
    return SW2 + (vm[0]*px + vm[1]*py + vm[2]*pz + vm[3]) * invW * SW2,
           SH2 - (vm[4]*px + vm[5]*py + vm[6]*pz + vm[7]) * invW * SH2
end

-- Main loop
for tick = 1, 999999 do
    local localPlayer = kernal.read_i64(base + OFF.local_player)
    if localPlayer == 0 then
        kernal.overlay_begin(); kernal.overlay_end()
        kernal.sync(); goto continue
    end

    local entities, entCount = scanPlayers(localPlayer)

    -- Mega read: localTeam + per-entity data + view render ptr
    -- STRIDE = hp(4)+maxHp(4)+team(4)+life(4)+shield(4)+shieldMax(4)+origin(12)+bleedout(4) = 40
    local STRIDE = 40
    -- ... build megaA, read_multi, parse buffer, draw boxes + bars ...

    -- View matrix via pointer chain (Apex-specific)
    local vrPtr = kernal.read_i64(base + OFF.view_render)
    if vrPtr ~= 0 then
        local matPtr = kernal.read_i64(vrPtr + OFF.view_matrix_off)
        if matPtr ~= 0 then
            local vmBuf = kernal.read_buf(matPtr, 64)
            for i = 0, 15 do vm[i] = vmBuf.f32(i*4) end
        end
    end

    kernal.overlay_begin()
    -- Draw entities with boxes, health bars, shield bars ...
    kernal.overlay_end()
    kernal.sync()
    ::continue::
end
© 2025 Kernal · v0.3 · kernal.vip