All work

C++ FORK

PetkosOrca

A C++ fork of OrcaSlicer in which one project targets a different printer on every plate, each drawn at that machine’s real bed size. The app ships as Podslicer; the repository keeps the petkos-orca name.

Read the source on GitHub
~30,000commits in the upstream codebase it extendsgit rev-list --count HEAD: 30,064
86.6 → 5.8 msmean GUI frame at 36 platesbefore/after runs, 731 and 720 frames
Per plateprinter, process and filaments, saved in the 3MFplater_printer_preset, excerpt below

RUNNING SOFTWARE

One project, seven machines

The plate board groups plates by the machine each one prints on and states that machine’s bed. The canvas draws every plate at its own printer’s size, so arranging onto a plate means arranging onto the real machine. Upstream closed the request for this as not planned.

PetkosOrca plate board showing 21 plates across 7 machines, grouped by printer with bed sizes 270 by 205, 250 by 250 and 300 by 300 millimetres, beside a 3D canvas of plates drawn at different sizes.
SCREENSHOT · 15 AUG 2026 21 plates across 7 machines, grouped by printer: 270 × 205, 250 × 250 and 300 × 300 mm beds in one project. “Unresolved” marks a plate whose material has no equivalent on its new printer; the fork reports it rather than guessing. Captured by the fork’s own screenshot harness.
PetkosOrca in its dark theme: a plate-order list where each row reads plate, arrow, printer, beside six plates on the canvas, the selected one drawn with its printer’s own bed texture.
SCREENSHOT · DARK THEME · 15 AUG 2026 Plate order view. Every row reads [plate] → [machine]; the selected plate wears its printer’s real bed texture and outline, the rest stay pale.

MECHANISM

What “per plate” means in the code

A plate owns a slicing context: printer, process and filament presets. The context resolves to a bed shape and print volume; arrange, fill-bed and boundary checks run per plate; each plate slices and exports its own G-code. The first version, 4cf3af2, touched 23 files, and its layout rewrite was checked byte-identical against the original single-bed path so existing projects arrange exactly as before.

  1. PLATE A plate owns a slicing context
  2. CONTEXT Printer, process, filaments saved per plate in the 3MF; absent in older projects
  3. RESOLVE Bed shape and volume from that plate’s printer preset
  4. LAYOUT Drawn at real size arrange, fill-bed and boundary checks per plate
  5. OUTPUT Its own G-code sliced with that plate’s resolved config
src/libslic3r/PlateSlicingContext.hpp commit fd10604f17 L34–40 on GitHub L46–51 on GitHub L67–75 on GitHub
// A plate's complete slicing identity, owned by the plate and by nothing else.
//
// There is no project printer to fall back to. An empty field is NOT inheritance: it is
// a plate that has not been given one yet, which happens in exactly two places - a plate
// that has just been created, and a project written before per-plate machines. Both are
// completed at once by PresetBundle::complete_plate_context, and after that every field
// is a name that must resolve exactly.

 
struct PlateSlicingContext
{
    std::string              printer_preset_name;
    std::string              printer_vendor_id;
    std::string              print_preset_name;
    std::vector<std::string> filament_preset_names;

 
    // Everything a slice needs to be named. printer_vendor_id is excluded: it is a
    // guard recorded against the printer name, not an identity of its own, and an
    // empty one means "no vendor was recorded", which resolves fine.
    // physical_printer_id is excluded too - which machine on the network prints this
    // is a dispatch question, and a plate slices without an answer to it.
    bool is_complete() const
    {
        return !printer_preset_name.empty() && !print_preset_name.empty() && !filament_preset_names.empty();
    }

The type the whole fork turns on, with the reasoning kept next to it. Upstream’s engine let an empty field mean “use the project’s printer”, which is exactly how a project could hold several printers in its data and still slice on one; deleting that meaning is what deletes the state.

src/libslic3r/Format/bbs_3mf.cpp commit fd10604f17 L4498–4502 on GitHub
            else if (key == PLATER_PRINTER_PRESET_ATTR) {
                m_curr_plater->slicing_context.printer_preset_name = xml_unescape(value.c_str());
            }
            else if (key == PLATER_PRINTER_VENDOR_ATTR) {
                m_curr_plater->slicing_context.printer_vendor_id = xml_unescape(value.c_str());

And the context read back out of the 3MF project file, so a saved project reopens on the same machines. These lines are the fork’s own; blame them on GitHub.

MEASURED

86.6 → 5.8 ms, one mechanism

The GUI lagged as plates were added. Instrumenting the frame found one cause wearing several costumes: a complete ~974-option configuration was composed 47 times a frame, 36 of them inside the per-volume draw loop, to read a colour. The fix hoists that read out of the loop and memoises the other caller once per frame. Method: the fork’s own span instrumentation, driven by a scripted 36-plate scene; one run before the change and one after, same machine and harness, the same afternoon. Only back-to-back comparisons like this are quoted, because heat alone moves a laptop’s absolute numbers. The commit message records the same figures.

Mean milliseconds per frame, 36-plate scene, same machine, same instrumented harness
  • Before the fix · 731 frames
  • After · 720 frames

Whole frame

Before: 86.580 ms mean86.6 ms After: 5.836 ms mean5.8 ms

Drawing the objects, per pass

Before: 30.449 ms mean30.4 ms After: 1.158 ms mean1.2 ms

Drawing the overlays

Before: 19.417 ms mean19.4 ms After: 1.384 ms mean1.4 ms
Table view
Span (mean, ms)BeforeAfter
Whole frame86.5805.836
Drawing the objects, per pass30.4491.158
Drawing the overlays19.4171.384
Whole frame, p99119.76113.382
Frames sampled731720
119.8 → 13.4 ms
worst 1% of frames (p99)
153 → 45 ms
switching plate, median time to first paint
34,586 → 2,289
config compositions per run
Full configuration compositions per rendered frame
Before 47.3
After 3.2

34,586 compositions over 731 frames, against 2,289 over 720. Each one deep-copies a ~974-option config to read a handful of values.

src/slic3r/GUI/3DScene.cpp commit 16615db75d L1155–1160 on GitHub
    //The extruder palette is identical for every volume in this loop, and obtaining it composes
    //the plate's whole ~974-option config. Asking once per volume cost 36 compositions - 53 ms -
    //in a 36-plate frame. The loop is here, so the hoist belongs here.
    const std::vector<ColorRGBA> extruder_colors = GUI::wxGetApp().plater()->get_extruders_colors();

    for (GLVolumeWithIdAndZ& volume : to_render) {

The hoist: one palette read per draw pass instead of one per volume. Later commits cache it per plate, because plates can now carry different filaments.

src/slic3r/GUI/Gizmos/GLGizmosManager.cpp commit 16615db75d L60–68 on GitHub L86–89 on GitHub
std::vector<size_t> GLGizmosManager::get_selectable_idxs() const
{
    //One composition per frame instead of eleven. See the note on the members in the header:
    //is_selectable() reaches GLGizmoMmuSegmentation, which composes the plate's entire config to
    //count filaments, and the toolbars ask this repeatedly while laying themselves out.
    const uint64_t frame = GLCanvas3D::frame_id();
    if (frame != 0 && frame == m_selectable_idxs_frame)
        return m_selectable_idxs_cache;


 
    m_selectable_idxs_cache = out;
    m_selectable_idxs_frame = frame;
    return out;
}

The memo: the gizmo toolbar asked eleven times a frame while laying itself out. Now it asks once, keyed on a frame counter.

IN AN INTERVIEW

The two-minute walkthrough

  1. 0:00

    Open the repository on its default branch, per-plate-machines.

    It is public and maintained, on top of a ~30,000-commit upstream.

  2. 0:20

    Open commit 4cf3af2, the first per-plate change.

    Scope inside a large C++ codebase, and a regression check on the path everyone else still uses.

  3. 0:50

    Open commit 16615db and read the message.

    Measurement before optimisation: cause, fix, rejected alternative and numbers in one place.

  4. 1:30

    Show upstream issue #7238.

    Why the fork exists: the feature was asked for and closed as not planned.

  5. 1:50

    If time allows, open Podslicer on the laptop and move a plate to another printer.

    The feature working, not described.

All work