Judge_Engine

The Judge Engine converts synchronized audio and input timing into gameplay events. In the current tree the public control surface is split between a runtime owner, PDJE_JUDGE::JUDGE, and an initialization bundle, PDJE_JUDGE::Judge_Init.

Judge Engine Architecture

The older docs spent more time on the judge internals, and that architecture is still useful for understanding how the public API is meant to be wired:

  • Judge_Init the setup bundle that collects core and input data lines, note objects, rail mappings, event rules, and optional custom callbacks

  • RAIL_DB the internal mapping database that links keyboard, mouse, or MIDI events to gameplay rails plus optional microsecond offsets

  • PDJE_Rule the area that defines EVENT_RULE and the input-key structures used for rail matching

  • InputParser the preprocessing stage that reads input events and normalizes them into a form the judge loop can consume

  • Judge_Loop the runtime loop that owns the timing process, input preprocessing, matching, and callback worker threads

  • Match the note-comparison logic that turns processed inputs into use or miss events

  • OBJ the note-object storage used by NoteObjectCollector() and the matcher

  • AxisModel still a placeholder in the current tree rather than a finished public feature

Runtime Types

class JUDGE

Judge controller that owns initialization data and the event loop.

enum PDJE_JUDGE::JUDGE_STATUS

Judge runtime status codes.

Values:

enumerator OK
enumerator CORE_LINE_IS_MISSING
enumerator INPUT_LINE_IS_MISSING
enumerator EVENT_RULE_IS_EMPTY
enumerator INPUT_RULE_IS_EMPTY
enumerator NOTE_OBJECT_IS_MISSING
class Judge_Init

Judge module initializer holding data lines, rules, and notes.

struct EVENT_RULE

Global hit window configuration in microseconds.

struct Custom_Events

Optional callback bundle used during judgment loop.

Startup Contract

JUDGE::Start() validates the initialization bundle before spawning the event loop thread.

JUDGE_STATUS PDJE_JUDGE::JUDGE::Start()

Validate init data and start the judge event loop thread.

void PDJE_JUDGE::JUDGE::End()

Stop the event loop and release cached init data.

Start will fail when any of the following are missing:

  • a valid core data line

  • a valid input data line

  • at least one note object

  • a populated EVENT_RULE

  • at least one rail mapping

Important current behavior:

  • Judge_Init::SetInputLine() only stores the line when input_arena is not null

  • MIDI rail mapping is supported, but the tested startup path still includes a configured standard input backend so the input line is considered valid

Initialization API

Selected setup methods:

void PDJE_JUDGE::Judge_Init::SetCoreLine(const PDJE_CORE_DATA_LINE &coreline)

Attach the core data line from PDJE core engine.

void PDJE_JUDGE::Judge_Init::SetInputLine(const PDJE_INPUT_DATA_LINE &inputline)

Attach the input data line from input engine.

void PDJE_JUDGE::Judge_Init::SetEventRule(const EVENT_RULE &event_rule)

Set judgment window configuration.

void PDJE_JUDGE::Judge_Init::SetCustomEvents(const Custom_Events &events)

Set optional callbacks for miss/use and mouse parsing.

void PDJE_JUDGE::Judge_Init::NoteObjectCollector(const std::string noteType, const uint16_t noteDetail, const std::string firstArg, const std::string secondArg, const std::string thirdArg, const unsigned long long Y_Axis, const unsigned long long Y_Axis_2, const uint64_t railID)

Collect note metadata and place it on the matching rail.

Judge_Init::SetRail(…) has three public overload groups:

  • standard devices: SetRail(const DeviceData&, const BITMASK, …)

  • MIDI by port name: SetRail(const std::string&, …)

  • MIDI by libremidi::input_port

All three overload groups map an input event to a gameplay rail plus an optional microsecond offset.

Timing Model

NoteObjectCollector() converts note timing from PCM-frame positions into microseconds through the 48 kHz conversion helpers defined in PDJE_Judge_Init_Structs.hpp. This lets the judge compare note timing with:

  • input timestamps emitted by the input engine

  • synchronized audio timing from audioSyncData

This is the key reason the judge is described as microsecond-oriented: timing precision is not limited to the audio callback cadence in the same way the older buffer-step model was.

AxisModel still exists only as a placeholder in the current source tree. Axis style note handling is not yet a complete public feature.

Binding Status

The older manual pages also documented a wrapper-facing judge flow:

  • the current in-tree SWIG C# and Python outputs do not expose PDJE_JUDGE::JUDGE

  • the non-C++ integration path that still exists in the docs is the Godot wrapper layer

  • that wrapper uses PDJE_Judge_Module and is designed to consume the Godot input wrapper plus the core/player wrapper

Reference Integration Order

The order below mirrors the integration flow in include/tests/JUDGE_TESTS/judgeTest.cpp.

PDJE engine("testRoot.db");
auto tracks = engine.SearchTrack("");
if (tracks.empty()) {
    return;
}

(void)engine.InitPlayer(PLAY_MODE::FULL_PRE_RENDER, tracks.front(), 480);

PDJE_Input input;
if (!input.Init()) {
    return;
}

auto devs = input.GetDevs();
auto midi_ports = input.GetMIDIDevs();

PDJE_JUDGE::JUDGE judge;
DEV_LIST selected_standard;

for (const auto &dev : devs) {
    if (dev.Type == PDJE_Dev_Type::KEYBOARD) {
        selected_standard.push_back(dev);
        judge.inits.SetRail(dev, PDJE_KEY::A, 0, 1);
    }
}

for (const auto &midi : midi_ports) {
    judge.inits.SetRail(
        midi,
        1,
        static_cast<const uint8_t>(libremidi::message_type::NOTE_ON),
        1,
        48,
        0);
}

if (!input.Config(selected_standard, midi_ports)) {
    return;
}

OBJ_SETTER_CALLBACK collect = [&](const std::string &noteType,
                                  const uint16_t noteDetail,
                                  const std::string &firstArg,
                                  const std::string &secondArg,
                                  const std::string &thirdArg,
                                  const unsigned long long y1,
                                  const unsigned long long y2,
                                  const uint64_t railId) {
    judge.inits.NoteObjectCollector(
        noteType, noteDetail, firstArg, secondArg, thirdArg, y1, y2, railId);
};

(void)engine.GetNoteObjects(tracks.front(), collect);

judge.inits.SetEventRule({
    .miss_range_microsecond = 600005,
    .use_range_microsecond = 600000,
});

judge.inits.SetInputLine(input.PullOutDataLine());
judge.inits.SetCoreLine(engine.PullOutDataLine());
judge.inits.SetCustomEvents({});

if (judge.Start() != PDJE_JUDGE::JUDGE_STATUS::OK) {
    return;
}

(void)input.Run();
(void)engine.player->Activate();

// ...

(void)engine.player->Deactivate();
input.Kill();
judge.End();

Callbacks

Custom_Events lets you attach:

  • missed_event

  • used_event

  • custom_mouse_parse

The judge loop keeps these callbacks off the main matching path by dispatching them through worker queues. Use lightweight handlers or your own downstream queueing if callback work can grow.

Practical callback example:

PDJE_JUDGE::MISS_CALLBACK missed =
    [](std::unordered_map<uint64_t, PDJE_JUDGE::NOTE_VEC> misses) {
        std::cout << "Miss groups: " << misses.size() << std::endl;
    };

PDJE_JUDGE::USE_CALLBACK used =
    [](uint64_t railid, bool pressed, bool is_late, uint64_t diff) {
        std::cout << railid << " " << pressed << " " << is_late
                  << " " << diff << std::endl;
    };

PDJE_JUDGE::MOUSE_CUSTOM_PARSE_CALLBACK mouse_parse =
    [](uint64_t microsecond,
       const PDJE_JUDGE::P_NOTE_VEC &found_events,
       uint64_t railid,
       int x,
       int y,
       PDJE_Mouse_Axis_Type axis_type) {
        (void)microsecond;
        (void)found_events;
        (void)railid;
        (void)x;
        (void)y;
        (void)axis_type;
    };

judge.inits.SetCustomEvents({
    .missed_event = missed,
    .used_event = used,
    .custom_mouse_parse = mouse_parse,
    .use_event_sleep_time = std::chrono::milliseconds(1),
    .miss_event_sleep_time = std::chrono::milliseconds(1),
});

Godot Wrapper Flow

$PDJE_Input_Module.Init()
var device_list:Array = $PDJE_Input_Module.GetDevs()
var selected_devices:Array = []
for device in device_list:
    if device["type"] == "MOUSE":
        selected_devices.push_back(device)

var selected_midi_devices = $PDJE_Input_Module.GetMIDIDevs()
$PDJE_Input_Module.Config(selected_devices, selected_midi_devices)

$PDJE_Judge_Module.AddDataLines($PDJE_Input_Module, engine)
for device in selected_devices:
    $PDJE_Judge_Module.DeviceAdd(device, 4, 0, InputLine.BTN_L, 0, 5)

for midi_port in selected_midi_devices:
    $PDJE_Judge_Module.MIDI_DeviceAdd(
        midi_port, 5, "NOTE_ON", 1, 48, 0)

$PDJE_Judge_Module.SetRule(60 * 1000, 61 * 1000, 1, 3, false)
$PDJE_Judge_Module.SetNotes(engine, "sample_track")

$PDJE_Judge_Module.StartJudge()
$PDJE_Input_Module.Run()
player.Activate()

player.Deactivate()
$PDJE_Input_Module.Kill()
$PDJE_Judge_Module.EndJudge()