Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Beef up sessions and scenarios #288

Closed
Capital-Asterisk opened this issue May 30, 2024 · 3 comments
Closed

Beef up sessions and scenarios #288

Capital-Asterisk opened this issue May 30, 2024 · 3 comments
Labels
enhancement New feature or request planned Likely a requirement for a 'final' product refactor Self explanatory tag name

Comments

@Capital-Asterisk
Copy link
Contributor

A few related things are required that are closely related:

A) Allow changing scenarios while the window is open

Requires a bit of refactoring to scenarios.

Sessions for keeping the window open (setup_window_app and setup_magnum) are already managed separately, found in main.cpp.

g_testApp.m_windowApp = scenes::setup_window_app (builder, g_testApp.m_topData, g_testApp.m_application);
g_testApp.m_magnum = scenes::setup_magnum (builder, g_testApp.m_topData, g_testApp.m_application, g_testApp.m_windowApp, MagnumApplication::Arguments{ argc, argv });

This means that scene-related sessions (setup_scene, setup_common_scene, setup_scene_renderer, setup_magnum_scene, setup_camera_ctrl, etc...) can be closed and new ones can be loaded in while the window stays open.

When a session is requested to change...

  1. Detect request to change session, likely at the end of CommonMagnumApp::draw.
  2. Call TestApp::close_sessions (runs all tasks with .cleanup(Run_)) so data can be destructed gracefully.
  3. Clear scene and scene renderer sessions.
  4. Call new scene setup and renderer setup functions.

B) Have some API to open new sessions

Imagine starting with just a main menu session that only uses UI. The user can then select and load up a flight scenario, launching new sessions (this is why sessions are called sessions).

Challenges:

  • Task graph execution has to stop and get cleared for new tasks to be added. Code that runs within tasks (literally everything in-game) can't modify the task graph and add Sessions and tasks directly; this would be ugly anyways. Task code has to send a request up to the top of the application to specify modifications of which sessions to add or remove.
  • Who handles drawing to the screen? Should responsibility of the main drawing function be passed between session tasks (this is the current case with setup_magnum_scene), or should there be one 'compositor' task/function at the top that commands how everything below it should be drawn through some API?
    Imagine a case where the user is sitting in a mission control room, and has screens with live camera views open of multiple missions distant from each other, all being simulated in separate scenes. This favors the compositor solution, where the currently visible scene can call render into the required flight scenes.

In a more finalized product, a list of flights scenes, their sessions, and additional data about them (eg: associations with the universe) can be stored somewhere at the top-level of the application. Renderers can then be optionally assigned to them for the application's compositor function to call into.

C) Automatic runtime Session dependencies

Right now, dependencies between sessions are intended to be easy to write and copy-paste around manually, relying on lists of variable names using macros. None of this can really be reconfigured to create custom scenarios at runtime.

Note how similar the physics test scenario and the vehicles test scenario is. You can just put if statements around the vehicle-related setup functions to turn the vehicles test scenario into the physics test scenario conditionally. It's possible to make one mega-sized scenario instead individual physics/vehicle/universe ones, but this can probably be done in a smarter way for the "more finalized product."

Ideally, the interface for this would be to just list off sessions, and dependencies will be automatically resolved. To do this, we need some way to identify/lookup sessions and determine what dependencies they require (make a trait system :3). A bunch of tables of data works for sure, but part of the problem is how to write a nice interface around it.

This might require a full rewrite of all the session stuff. Sessions are just groups of tasks, data, and pipelines, and maybe dealing with them individually 'per-scene' and removing sessions entirely may be easier for an automated solution. we'll see.

@Capital-Asterisk Capital-Asterisk added enhancement New feature or request refactor Self explanatory tag name planned Likely a requirement for a 'final' product labels May 30, 2024
@Capital-Asterisk
Copy link
Contributor Author

Here's a bit of a mockup of a new interface I'm planning:

Before

src/testapp/identifiers.h:

#define TESTAPP_DATA_PHYS_SHAPES 1, \
    idPhysShapes
struct PlPhysShapes
{
    PipelineDef<EStgIntr> spawnRequest      {"spawnRequest      - Spawned shapes"};
    PipelineDef<EStgIntr> spawnedEnts       {"spawnedEnts"};
    PipelineDef<EStgRevd> ownedEnts         {"ownedEnts"};
};

src/testapp/sessions/physics.cpp:

Session setup_phys_shapes(
        TopTaskBuilder&             rBuilder,
        ArrayView<entt::any> const  topData,
        Session const&              scene,
        Session const&              commonScene,
        Session const&              physics,
        MaterialId const            materialId)
{
    OSP_DECLARE_GET_DATA_IDS(commonScene,   TESTAPP_DATA_COMMON_SCENE);
    OSP_DECLARE_GET_DATA_IDS(physics,       TESTAPP_DATA_PHYSICS);
    auto const tgScn    = scene         .get_pipelines<PlScene>();
    auto const tgCS     = commonScene   .get_pipelines<PlCommonScene>();
    auto const tgPhy    = physics       .get_pipelines<PlPhysics>();

    Session out;
    OSP_DECLARE_CREATE_DATA_IDS(out, topData, TESTAPP_DATA_PHYS_SHAPES);
    auto const tgShSp = out.create_pipelines<PlPhysShapes>(rBuilder);

    rBuilder.pipeline(tgShSp.spawnRequest)  .parent(tgScn.update);
    rBuilder.pipeline(tgShSp.spawnedEnts)   .parent(tgScn.update);
    rBuilder.pipeline(tgShSp.ownedEnts)     .parent(tgScn.update);

    top_emplace< ACtxPhysShapes > (topData, idPhysShapes, ACtxPhysShapes{ .m_materialId = materialId });

    rBuilder.task()
        .name       ("Schedule Shape spawn")
        .schedules  ({tgShSp.spawnRequest(Schedule_)})
        .sync_with  ({tgScn.update(Run)})
        .push_to    (out.m_tasks)
        .args       ({           idPhysShapes })
        .func([] (ACtxPhysShapes& rPhysShapes) noexcept -> TaskActions
    {
        return rPhysShapes.m_spawnRequest.empty() ? TaskAction::Cancel : TaskActions{};
    });
// ...

src/testapp/scenarios.cpp:

#define SCENE_SESSIONS      scene, commonScene, physics, physShapes, droppers, bounds, newton, nwtGravSet, nwtGrav, physShapesNwt
#define RENDERER_SESSIONS   sceneRenderer, magnumScene, cameraCtrl, cameraFree, shVisual, shFlat, shPhong, camThrow, shapeDraw, cursor

using namespace testapp::scenes;

auto const  defaultPkg      = rTestApp.m_defaultPkg;
auto const  application     = rTestApp.m_application;
auto        & rTopData      = rTestApp.m_topData;

TopTaskBuilder builder{rTestApp.m_tasks, rTestApp.m_scene.m_edges, rTestApp.m_taskData};

auto & [SCENE_SESSIONS] = resize_then_unpack<10>(rTestApp.m_scene.m_sessions);

// Compose together lots of Sessions
scene           = setup_scene               (builder, rTopData, application);
commonScene     = setup_common_scene        (builder, rTopData, scene, application, defaultPkg);
physics         = setup_physics             (builder, rTopData, scene, commonScene);
physShapes      = setup_phys_shapes         (builder, rTopData, scene, commonScene, physics, sc_matPhong);
// ...
  • I guess there's some benefits to manually specifying all dependencies between sessions/features, but is rather cumbersome.

After

  • Renaming "Sessions" -> "Feature".
    • These are originally called "sessions" because it's something that isn't really a single 'process' that opens, does something, interacts with other things, then closes. This terminology is similar to how something like a web browser session uses an HTTP session which uses a TCP session. Switching the terminology is like saying "a feature is a session" instead of a "session is a feature."
  • Do lots of stuff to reduce the stupid amount of boilerplate required.

src/testapp/identifiers.h:

struct FIPhysShapes
{
    struct DataIds
    {
        TopDataId physShapes;
    };

    struct Pipelines
    {
        PipelineDef<EStgIntr> spawnRequest      {"spawnRequest      - Spawned shapes"};
        PipelineDef<EStgIntr> spawnedEnts       {"spawnedEnts"};
        PipelineDef<EStgRevd> ownedEnts         {"ownedEnts"};
    };
};
  • FI for "Feature Interface"
  • Prevents Features from DIRECTLY depending on each other. (FI adds an extra level of indirection. Fundamental theorem of software engineering fixes everything)
  • One feature would implement_interface a FI, so that other features can use_interface it.
  • Features can implement multiple FIs, but each FI can only be implemented by one Feature.
  • Of course this is inspired by Rust traits in some way.

src/testapp/sessions/physics.cpp:

void ft_phys_shapes( FeatureBuilder fb )
{
    fb.name("Physics Shapes");

    auto [diScn, plScn] = fb.use_interface<FIScene>();
    auto [diCS,  plCS]  = fb.use_interface<FICommonScene>();
    auto [diPhy, plPhy] = fb.use_interface<FIGenericPhysics>();

    auto [dPhSh, lPhSh] = fb.implement_interface<FIPhysShapes>();

    fb.pipeline(lPhSh.spawnRequest)   .parent(plScn.update);
    fb.pipeline(lPhSh.spawnedEnts)    .parent(plScn.update);
    fb.pipeline(lPhSh.ownedEnts)      .parent(plScn.update);

    fb.data(dPhSh.physShapes).emplace< ACtxPhysShapes >( ACtxPhysShapes{ .m_materialId = materialId } );

    fb.task()
        .name       ("Schedule Shape spawn")
        .schedules  ({lPhSh.spawnRequest(Schedule_)})
        .sync_with  ({plScn.update(Run)})
        .args       ({       dPhSh.physShapes })
        .func([] (ACtxPhysShapes& rPhysShapes) noexcept -> TaskActions
    {
        return rPhysShapes.m_spawnRequest.empty() ? TaskAction::Cancel : TaskActions{};
    });
// ...
  • Features are created at runtime. This ensures there's still ways to make them dynamically loadable or through a scripting language or whatnot.
  • Some error handling is preferred that isn't exceptions nor aborting.
  • materialId here doesn't have a definition. I haven't quite figured out the 'best' way to pass config data down to feature setups.
  • Might change this to allow specifying dependencies to Feature Interfaces elsewhere, so they're accessible before this function is even called. Haven't quite figured out the least-ugly way to do this though.

src/testapp/scenarios.cpp:

auto ctxBuilder = rTestApp.builder().create_context();

rTopData.m_currentScene = ctxBuilder.context_id();

ctxBuilder.add_feature(&ft_scene);
ctxBuilder.add_feature(&ft_scene_common);
ctxBuilder.add_feature(&ft_physics);
ctxBuilder.add_feature(&ft_phys_shapes);
// ...
  • Oh thank god
  • "context" is used to differentiate between different scenes. If there was two scenes, we'd want to specify which one physics shapes is added to.

@Capital-Asterisk
Copy link
Contributor Author

putting this here to not be so dependent on discord.

A couple iterations later: https://godbolt.org/z/Gb3GvPqar

C++ metaprogramming is inherently a little cursed but that's that.

I'm not expecting many people to understand how the template nonsense works (I put quite a bit of effort into making it actually readable, though it would be a fun challenge to dive into).

I mostly care about the user code below, which can drastically reduce the amount of boilerplate in testapp.

Calls to fb.use_interface<...> and fb.implement_interface<...> from the previous comment won't be needed anymore as function arguments are instead read to determine dependencies:

auto const gc_ftrPhysShapes = feature_def([] (FeatureBuilder& rBuilder, Implement<FIPhysShapes> phySh, DependOn<FIScene> scn, DependOn<FIGenericPhysics> phys)
{
    // Initialize values of TopData (top_emplace),
    // setup pipelines (rBuilder.pipeline(...).parent(...)),
    // and setup tasks here
});

quoting my previous comment:

allow specifying dependencies to Feature Interfaces elsewhere, so they're accessible before this function is even called

@Capital-Asterisk
Copy link
Contributor Author

#298

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
enhancement New feature or request planned Likely a requirement for a 'final' product refactor Self explanatory tag name
Projects
None yet
Development

No branches or pull requests

1 participant