Skip to content

Glk Multi Display Support

Andrew Plotkin edited this page Mar 14, 2015 · 6 revisions

The Glk API assumes that the application is managing exactly one Glk game display at a time. This fits poorly with the modern desktop GUI, in which a game interpreter might want to display several windows with a different game in each.

Interpreter applications typically work around this by launching sub-processes, each of which manages one game in one window. This is adequate, but we would like a better solution.

This is a proposal for a Glk interface update which supports multiple displays within a single application, without breaking backwards compatibility.

(I'm saying "display" here because "window" already has a meaning in Glk.)

(This plan deals only with the C header glk.h and C library implementations. The Javascript GlkOte implementation already supports multiple Glk displays in separate browser windows -- each browser window is a separate Javascript context, so we get that for free. It could be extended to support multiple Glk displays in a single browser window, but this is not very important.)

Changes to the glk.h header

The glk.h header should serve both old (single-display) libraries and new (multi-display) libraries. Therefore, we need some preprocessor abuse!

Function definitions will now look like:

/* extern void glk_put_char(unsigned char ch); */
GLK_DECLARE_FN1(void, put_char, unsigned char ch);

(GLK_DECLARE_FN1 means "declare Glk function with one argument". GLK_DECLARE_FN0 to GLK_DECLARE_FN6 will be available.)

If the symbol GLK_LIBRARY_MULTI is not defined, this will compile to the old-style declaration, as shown in the comment. If GLK_LIBRARY_MULTI is defined, it compiles to two declarations, one with an extra argument:

extern void glk_put_char(unsigned char ch);
extern void glkmd_put_char(glk_context *gcontext, unsigned char ch);

The library will maintain a global variable:

extern glk_context *glk_context_singleton;

A call to the old-style function will invoke the new-style function using this context. That is, glk_put_char(ch) will be implemented as a call to glkmd_put_char(glk_context_singleton, ch). This allows old-style applications to compile with minimal change.

(These boilerplate old-style functions will live in a new file gi_multi.c, which will be distributed along with gi_dispa.c and gi_blorb.c.)

The declaration glk_main() will be replaced by glkmd_main(context). (Not duplicated, but replaced! If GLK_LIBRARY_MULTI is defined, glk_main() is not called.) These functions must be defined by the application.

Note that some functions, such as glk_char_to_lower(), don't interact with the display state. For consistency, these will also get glkmd_XXX(context) implementations, but they can ignore the context argument.

Changes to gi_dispa.c

When GLK_LIBRARY_MULTI is set, the gidispatch_function_t struct will contain pointers to both the glk_XXX() and glkmd_XXX() versions of each function. (This will entail more preprocessor abuse.)

The gidispatch_call() entry point will remain, calling glk_XXX() functions. Under GLK_LIBRARY_MULTI, there will also be gidispatch_call_multi(), with an additional context argument, which calls the glkmd_XXX() functions. (I haven't decided what kind of evil I will use to accomplish this, but nobody else ever has to read gi_dispa.c, so don't sweat it.)

Changes to libraries

An old-style library can import the new glk.h header file, and not define GLK_LIBRARY_MULTI. It will build the way it's always built, no changes needed.

To fully support multi-display Glk, a library will have to be refactored so that all Glk display state is contained in a glk_context struct. (The library must define this struct type.) It will have to rename all its glk_XXX() functions to glkmd_XXX(), adding a glk_context * as the first argument.

(The library will not have to define glk_XXX() functions. Those will be provided by gi_multi.c.)

(CheapGlk, GlkTerm and iOSGlk will remain as old-style, single-display libraries. RemGlk will become a multi-display library, although this is more a proof of concept than a useful feature. A RemGlk interpreter can be launched with multiple games; input and output for each game will be tagged with a distinct session ID.)

Changes to library startup code

The library startup code is the C main() function. In an old-style library this checks command-line arguments, initializes everything, runs glkunix_startup_code() (application-specific startup code), and then calls glk_main() followed by glk_exit().

A multi-display library is different. The obvious case is a Mac/Windows application which can open multiple game windows. A library built to support this will launch with some kind of file selection UI. The user can select additional game files at any time; each one becomes a new Glk context. There will also be some UI to destroy a context (e.g., a window-close button).

This will probably require two startup functions. There might be a glkosx_startup_code() to set up the UI toolkit and display the file selection UI. Each file selection triggers a glkosx_startcontext_code() call, which would open the game file and prepare the game, followed by a glkmd_main() call.

On the other hand, not all applications will behave this way. You might build a standalone game application which launches straight into the game. So the glkosx_startup_code() call will need a way to signal this. If it signals "wait", the library startup code will wait for files to be chosen (however that works). If the glkosx_startup_code() call signals "immediate", the library startup code will proceed directly to glkosx_startcontext_code() and then glkmd_main().

Either way, it is the library's job to allocate a glk_context and pass it to the startup functions and then to glkmd_main(). If there will only be one context, it should be stored as glk_context_singleton.

(I am not specifying the startup APIs beyond these requirements. These are up to the library, although of course it's nice if they all follow the same pattern. For backwards compatibility, "immediate" should be the default behavior.)

Changes to applications that don't care about this stuff

A multi-display library always calls glkmd_main() rather than glk_main(), because it has to pass in the context.

To compile a Glk application with a new library therefore requires some changes. However, if you want to keep using a single display, the changes will be minimal.

You'll choose the "immediate" path (see above). The application will still need a glkmd_main() function, but you can simply drop in this code:

#ifdef GLK_LIBRARY_MULTI
void glkmd_main(glk_context *gcontext) {
  glk_main();
}
#endif /* GLK_LIBRARY_MULTI */

The argument in this case will be glk_context_singleton, so the existing glk_main() implementation will continue to work.

Changes to applications to support multiple displays

(This section will remain somewhat vague until a multi-display Glulxe has been demonstrated. I know, it's the most important part. Sorry...)

Separate your startup code into application-launch and context-launch, as described above.

Rename your glk_main() function to glkmd_main(), with a context argument. Change all of your glk_XXX() function calls to glkmd_XXX(), passing the context argument back in.

You may associate arbitrary extra information with the context:

void gimulti_context_set_rock(glk_context *gcontext, void *rock);
void *gimulti_context_get_rock(glk_context *gcontext);

The structure of your application depends on what you're doing. RemGlk will be a single-threaded library; it will always be waiting for JSON input on stdin. The session ID of the input will indicate which Glk context to fire up and pass the input to. It will keep running until every context has exited.

For a desktop GUI library, it will probably make sense to give each Glk context its own thread. The UI will be handled by the application's main thread, and each glkmd_main() will run in its own "VM thread". (See Threading in Glk Libraries. This model can easily be extended to multiple threads and contexts.) ("Easily". No, really.)

As noted in the Threading document, the context will remain around after glkmd_main() returns or calls glkmd_exit(). It will effectively be "waiting for no input", while allowing the user to scroll and read text. It is the user's job to close the context (the UI window) and thus destroy the glk_context.

(Of course, it's also possible that the user will do this while glkmd_main() is still running -- that is, in the middle of the game.)

(I have not yet worked out the cleanup sequence, which will have to ensure that both the glk_context and the application state are tidied up. Note the three possible exit paths: glkmd_exit() call, glkmd_main() returning, and user close command. Again, I apologize for the haziness.)