Multiple design support

This document describes how multiple design and project files are supported starting from librnd 4.0.0.

Architecture

There is a global variable in librnd, called rnd_designs. This is a doubly linked list holding all designs currently open, each an (rnd_design_t *). There is a hash of project files (key is realpath): each project file opened and cached in memory only once, referenced by one or more of the design files.

Globally there is a "current design", which also determines the "current project" (via it's parent project pointer).

Note: a design (rnd_design_t *) represents a board file in pcb-rnd, a sheet in sch-rnd.

What's in a design

All actual design data is on the app's side, where rnd_design_t MUST be the first field of the design structure. On librnd side rnd_design_t contains:

What's in a project

A project structure reflects a project file. A project file contains configuration (and design file list) for multiple tools. Librnd will handle the subtree for related to the current application only. For example a typical ringdove project file will contain a subtree for sch-rnd and another for pcb-rnd. When librnd linked in pcb-rnd is dealing with the project file, it will read only the pcb-rnd subtree but will preserve the sch-rnd and other subtrees.

For applications implemented using librnd, the app-specific subtree of the project file is a config tree. Thus the (optional) design file list of the project is a config list (which can be specified only at project role).

When a project file is open via UI, what really happens is:

  1. creating a project structure
  2. loading the application specific config tree of the project file
  3. loading all designs listed in that config tree
  4. if there's no design listed, the operation fails and the project structure is discarded

When an individual design file is open via UI:

  1. the design is loaded
  2. if a project file is present in the same directory as the design file, it is assumed to be the project file for the given design, independently of whether it lists the design file on its file listing; such project file is loaded, and the design is put into that project in memory (without altering the project's design file listing)
  3. if there's no such project file, a dummy project file is created in memory; a dummy project file is not saved until the user makes explicit UI commands (e.g. changing config settings on project file level)

Each project file (as per realpath) is stored only once in memory, and has only one project structure. Every design file references to one of the project file structures in memory. Every project file in memory has an internal, only-in-memory list of design structures that references it.

If, due to design unloading, a project structure's internal design list becomes empty, the project structure is unloaded too.

Thus the project structure contains:

Since the project file may be used by multiple applications in parallel, any change to the project config (of non-dummy project files) is saved to disk as soon as possible.

A design that is part of a project in-memory is not always explicitly part of a project as per configured design file list. This can happen if:

UI needs to be provided for the user to indicate this situation and shortcuts should be provided so that a design file already associated to a project structure in memory can be made an explicit listed design file in the project config tree, or a design file part of the config tree can be removed from there.

How config is managed

Runtime configuration, or config for short, is stored in read-only global variables (structs) for quick access. There are multiple such global config variables:

Upon switching the current design, global config states are saved in a field of the design we are switching away from and then the global config states are loaded from the design we are switching to. This way global config states always reflect the config state of the current design.

There's an API provided for plugins to register their config states: rnd_conf_plug_reg(). This in turn calls rnd_conf_state_plug_reg(), which registers the global var and its size in the list of "need to save/load on switch".

The app's conf_core is registered on the same list, by the app, using rnd_conf_state_plug_reg().

Why config is not stored in rnd_design_t

Saves and loads are done using memcpy. A typical config struct ranges from a few dozen bytes to a few hundred bytes. Which means a full switch takes a handful of memcpy()s moving at most a few kilobytes. Compared to the frequency of design switches and all the GUI refresh implications, this cost is reasonable.

An earlier plan was to move rnd_conf and conf_core into the design struct so each design really has its won, then every single time the code needs to access the config, it needs to do so using a design (e.g. the current design). Besides the code complication on each config access, the main problem with this approach is that it can't deal with dynamic config vars supplied by the modules without making config access from modules real complicated.

Local vs. global access

librnd core

Librnd is aware of the current design (see rnd_multi_*), the list of designs loaded and their project files. Apps should use the core librnd API to manipulate current design or the list of designs.

UI

For example the UI is dealing with one design at a time: the UI state, accessed with ->set_hidlib() and ->get_hidlib(), stored by the HID. In any GUI event, such as dialog box callbacks on user input events, the related (rnd_design_t *) is delivered. This means dialog boxes are always bound to a specific design and they remember their (rnd_design_t *) and pass that on in one way or another to any of their callbacks.

There is an event called RND_EVENT_DESIGN_SET_CURRENT for switching the current design shown on the GUI. If a dialog box is global:

A typical example of "single instance global dialog" is the preferences dialog. Only one instance can be open, and it changes its contents when the current design is switched.

The other type of dialog box is local, which presents states/data of a given design and is not affected by RND_EVENT_DESIGN_SET_CURRENT.

A typical example of a "multiple instance local dialog" is pcb-rnd's propedit dialog: there can be multiple propedits open, but each is bound to a specific board, listing and manipulating objects of only that one board.

events (rnd_event())

Event callbacks get a (rnd_design_t *). There are two kind of events:

Each event must be marked as [d] or [a] in the documentation (comment).

config change callbacks

These are always local, always delivering a (hidlib_t *) that is affected. There are cases when a config change affects multiple open designs:

In such case librnd delivers a separate config change callback for each design affected.

Special use case: single-design app

An application may decide not to use multiple designs. This simplifies the code because there's always one active design, in all respects, and it can be achieved from the HID struct if nothing else offers it.

This is indicated in rnd_app.single_design. Librnd internals are still all prepared for multiple designs and related project administration, but when a new design is registered, it is not appended to the list of designs but replaces the previous design there. So the list of designs is always of size 1.

Instead of rnd_multi_* API, such app should use rnd_single_*.