diff --git a/MUNGA_L4/L4APP.H b/MUNGA_L4/L4APP.H index 9d65613..d01c3e2 100644 --- a/MUNGA_L4/L4APP.H +++ b/MUNGA_L4/L4APP.H @@ -79,6 +79,9 @@ public: static unsigned int GetScreenWidth() { return mScreenWidth; } static unsigned int GetScreenHeight() { return mScreenHeight; } static bool GetFullscreen() { return mFullscreen; } + // -fit: borderless window filling the monitor, with the render size + // chosen to match the cockpit canvas it will be presented into. + static bool GetFitDisplay() { return mFitDisplay; } static Logical GetSeeSolids() { return seeSolids; } static unsigned long GetNetworkCommonFlatAddress() { return networkCommonFlatAddress; } // The front end's multiplayer path turns network mode on at launch @@ -93,10 +96,17 @@ public: static CString GetRIORecordingFileName() { return rioRecordingFileName; } protected: + // Pick mScreenWidth/mScreenHeight from the monitor. Runs after the + // whole command line is parsed, so an explicit -res always wins no + // matter which side of -fit it appears on. + static void ChooseFitResolution(); + static HINSTANCE mhInstance; static unsigned int mScreenWidth; static unsigned int mScreenHeight; static bool mFullscreen; + static bool mFitDisplay; + static bool mResExplicit; static Logical seeSolids; static CString eggNotationFileName; static CString spoolFileName; diff --git a/MUNGA_L4/L4APP.cpp b/MUNGA_L4/L4APP.cpp index 1476fdc..fd4698c 100644 --- a/MUNGA_L4/L4APP.cpp +++ b/MUNGA_L4/L4APP.cpp @@ -94,6 +94,8 @@ HINSTANCE L4Application::mhInstance = NULL; unsigned int L4Application::mScreenWidth = 800; unsigned int L4Application::mScreenHeight = 600; bool L4Application::mFullscreen = true; +bool L4Application::mFitDisplay = false; +bool L4Application::mResExplicit = false; Logical L4Application::seeSolids = False; CString L4Application::eggNotationFileName; CString L4Application::spoolFileName; @@ -205,6 +207,7 @@ Logical { mScreenWidth = atoi(W2A(argv[++(*arguement)])); mScreenHeight = atoi(W2A(argv[++(*arguement)])); + mResExplicit = true; } else { @@ -217,18 +220,88 @@ Logical mFullscreen = false; } + //------------------------------------------------------------------- + // -fit: borderless window over the whole monitor, and a render size + // picked to match. Spelled out as -windowed-fullscreen too, since + // that is what the rest of the world calls it. + //------------------------------------------------------------------- + else if ( + !stricmp(W2A(argv[*arguement]), "-fit") || + !stricmp(W2A(argv[*arguement]), "-windowed-fullscreen") + ) + { + mFullscreen = false; + mFitDisplay = true; + } + else if ( !stricmp(W2A(argv[*arguement]), "-h") || !stricmp(W2A(argv[*arguement]), "-help") ) { DEBUG_STREAM << "\n" << argv[0] << - " -egg -net -solids -h -help\n"; + " -egg -net -solids -h -help\n" + " -windowed windowed, title bar and all\n" + " -fit borderless over the whole monitor,\n" + " render size chosen to match\n" + " (long form: -windowed-fullscreen)\n" + " -res explicit render size; overrides the\n" + " size -fit would have picked\n"; return False; } return True; } +// +//############################################################################# +// ChooseFitResolution +//############################################################################# +// +// The cockpit presents the 3D into a viewscreen that fills its 1920x1080 +// canvas, and that canvas is fitted to the window at one uniform scale. +// So the render size that lands 1:1 on screen is the canvas at the same +// scale the cockpit will choose - work it out with identical arithmetic +// here and the stretch becomes a copy. +// +void + L4Application::ChooseFitResolution() +{ + int monitor_w = GetSystemMetrics(SM_CXSCREEN); + int monitor_h = GetSystemMetrics(SM_CYSCREEN); + if (monitor_w <= 0 || monitor_h <= 0) + { + return; + } + + const int canvas_w = 1920; + const int canvas_h = 1080; + + int fit_w = (monitor_w * 100) / canvas_w; + int fit_h = (monitor_h * 100) / canvas_h; + int scale = (fit_w < fit_h) ? fit_w : fit_h; + if (scale < 25) scale = 25; + + unsigned int width = (canvas_w * scale) / 100; + unsigned int height = (canvas_h * scale) / 100; + + //------------------------------------------------------------------- + // A 1995 engine on a 5K panel would be asked for a back buffer well + // past anything it was built for; cap and let D3D scale the last bit. + //------------------------------------------------------------------- + if (width > 3840) + { + width = 3840; + height = 2160; + } + + mScreenWidth = width; + mScreenHeight = height; + + DEBUG_STREAM << "L4Application: -fit chose -res " << width << " " + << height << " (" << scale << "% canvas) for the " + << monitor_w << "x" << monitor_h << " monitor\n" << std::flush; +} + // //############################################################################# // ParseCommandLine @@ -270,6 +343,12 @@ Logical } } } + + // after the whole line, so -res wins from either side of -fit + if (mFitDisplay && !mResExplicit) + { + ChooseFitResolution(); + } return True; } diff --git a/MUNGA_L4/L4MFDVIEW.cpp b/MUNGA_L4/L4MFDVIEW.cpp index c4fe40b..e3fccb4 100644 --- a/MUNGA_L4/L4MFDVIEW.cpp +++ b/MUNGA_L4/L4MFDVIEW.cpp @@ -17,9 +17,26 @@ namespace // player actually clicks. Applies to both the MFD strips (depth // measured vertically) and the map's side columns (horizontally). //--------------------------------------------------------------- - const int buttonDepth = 100; + // 240 against the 480 glass: the two banks reach in far enough to + // meet in the middle bar the strips' own width, so practically the + // whole display is a press target and neither bank overlaps the + // other. The clamps below keep that true on a scaled-down cockpit. + const int buttonDepth = 240; const int indicatorStrip = 10; + //--------------------------------------------------------------- + // The map paints its own legend beside each side button, and that + // grid is not the naive height/6: measured off the 640-tall map, + // the first cell starts 13 rows down and the six cells are 102 + // tall on a 105 pitch (13 + 6*102 + 5*3 = 640). Spacing the + // buttons evenly from the top instead left every one of them + // riding high of its label. Scaled to the live glass height. + //--------------------------------------------------------------- + const int mapLegendSpan = 640; + const int mapLegendTop = 13; + const int mapLegendCell = 102; + const int mapLegendPitch = 105; + //--------------------------------------------------------------- // Button fill colors by lamp brightness (0 off / 1-2 dim / 3 // bright), red family for MFD strips, amber for the side columns. @@ -327,20 +344,28 @@ void if (button_w > limit) button_w = limit; if (button_w < strip_w) button_w = strip_w; - int button_h = display_height / 6; - displayX = strip_w; *client_width = display_width + 2 * strip_w; for (int i = 0; i < 6; ++i) { - int h = (i == 5) ? (display_height - 5 * button_h) : button_h; + //------------------------------------------------------- + // Take top and bottom off the legend grid separately and + // subtract: scaling a height directly would let rounding + // drift the buttons out of step with the labels. + //------------------------------------------------------- + int cell_top = mapLegendTop + i * mapLegendPitch; + int top = (cell_top * display_height) / mapLegendSpan; + int bottom = + ((cell_top + mapLegendCell) * display_height) / mapLegendSpan; + int h = bottom - top; + if (h < 1) h = 1; ScreenButton *left = &buttons[buttonCount++]; left->unit = anchorA + i; left->amber = 1; left->x = 0; - left->y = i * button_h; + left->y = top; left->w = button_w; left->h = h; diff --git a/MUNGA_L4/L4VB16.cpp b/MUNGA_L4/L4VB16.cpp index 7113ce6..2684533 100644 --- a/MUNGA_L4/L4VB16.cpp +++ b/MUNGA_L4/L4VB16.cpp @@ -3844,6 +3844,323 @@ static LRESULT CALLBACK SVGA16 *SVGA16::activeCockpit = NULL; +//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +// The six secondary displays at their pod sizes, in canvas units. +//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +static const int cockpitMfdBaseW = 320; +static const int cockpitMfdBaseH = 240; +// the radar reads best big: 1.35x the compact glass +// (1.5x proved a touch too large in playtest) +static const int cockpitRadarBaseW = 324; +static const int cockpitRadarBaseH = 432; + +//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +// A percentage from the environment, falling back to the given default +// when the variable is unset, unreadable or nonsense. That fallback is +// how a per-display setting inherits the group one. +//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +static int + DisplayScalePercent(const char *variable, int fallback) +{ + const char *text = getenv(variable); + if (text == NULL) + { + return fallback; + } + + int percent = atoi(text); + if (percent <= 0) + { + return fallback; + } + if (percent < 25) percent = 25; + if (percent > 200) percent = 200; + return percent; +} + +//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +// Shrink a display to fit a limit, uniformly. Clamping one axis alone +// would stretch the glass out of shape, and these are photographs of +// real instruments - they have to keep their proportions. +//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +static void + ClampGlass(SVGA16::GlassSize *glass, int max_w, int max_h) +{ + if (max_w < 1) max_w = 1; + if (max_h < 1) max_h = 1; + + if (glass->w > max_w) + { + glass->h = (glass->h * max_w) / glass->w; + glass->w = max_w; + } + if (glass->h > max_h) + { + glass->w = (glass->w * max_h) / glass->h; + glass->h = max_h; + } + if (glass->w < 1) glass->w = 1; + if (glass->h < 1) glass->h = 1; +} + +//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +// Where the radar sits. The pod had it dead centre under the +// viewscreen, which on a wide panel is exactly where the road is, so +// L4RADARPOS moves it out of the way: either bottom corner, or halfway +// up either side. +// +// On the bottom row it is one of three panes and the lower MFD whose +// corner it takes slides inboard beside it. Halfway up a side it leaves +// the bottom row altogether and sits between that side's two MFDs. +//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +enum RadarPlacement +{ + RadarBottomCenter = 0, + RadarBottomLeft, + RadarBottomRight, + RadarMidLeft, + RadarMidRight +}; + +static Logical + RadarOnASide(RadarPlacement placement) +{ + return (placement == RadarMidLeft || placement == RadarMidRight) + ? True : False; +} + +static Logical + RadarOnTheLeft(RadarPlacement placement) +{ + return (placement == RadarBottomLeft || placement == RadarMidLeft) + ? True : False; +} + +static RadarPlacement + RadarPosition() +{ + static int cached = -1; + + if (cached < 0) + { + //--------------------------------------------------------------- + // Two spellings each: the short ones were here first, the long + // ones say which edge out loud now that there are five. + //--------------------------------------------------------------- + static const struct + { + const char *name; + int placement; + } + names[] = + { + { "CENTER", RadarBottomCenter }, + { "CENTRE", RadarBottomCenter }, + { "BOTTOM", RadarBottomCenter }, + { "LEFT", RadarBottomLeft }, + { "BOTTOMLEFT", RadarBottomLeft }, + { "RIGHT", RadarBottomRight }, + { "BOTTOMRIGHT", RadarBottomRight }, + { "MIDLEFT", RadarMidLeft }, + { "LEFTCENTER", RadarMidLeft }, + { "LEFTCENTRE", RadarMidLeft }, + { "MIDRIGHT", RadarMidRight }, + { "RIGHTCENTER", RadarMidRight }, + { "RIGHTCENTRE", RadarMidRight } + }; + + cached = RadarBottomCenter; + + const char *text = getenv("L4RADARPOS"); + if (text != NULL) + { + for (int i = 0; i < (int) (sizeof(names) / sizeof(names[0])); ++i) + { + if (!stricmp(text, names[i].name)) + { + cached = names[i].placement; + break; + } + } + } + + static const char *described[] = + { + "bottom centre", + "bottom left", + "bottom right", + "left side, centred", + "right side, centred" + }; + DEBUG_STREAM << "SVGA16: radar on the " << described[cached] + << "\n" << std::flush; + } + return (RadarPlacement) cached; +} + +//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +// The six secondary displays sized for a given canvas scale. +// +// The pod bolted these down at fixed sizes; a desktop panel has room to +// trade viewscreen for instrument, so the player scales them. Each has +// its own setting, falling back to the group one - L4MFDSCALE sets all +// five MFDs, L4MFDSCALE_UL and friends override individually, and +// L4RADARSCALE the radar. +// +// Scaling happens in canvas units, before the canvas is fitted to the +// window, so a given number means the same thing on every monitor. +//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +void + SVGA16::CockpitGlassSizes(int scale, GlassSize sizes[SplitViewCount]) +{ + static Logical settings_read = False; + static int percent[SplitViewCount]; + + if (!settings_read) + { + settings_read = True; + + int group = DisplayScalePercent("L4MFDSCALE", 100); + percent[SplitMFDUpperLeft] = + DisplayScalePercent("L4MFDSCALE_UL", group); + percent[SplitMFDUpperCenter] = + DisplayScalePercent("L4MFDSCALE_UC", group); + percent[SplitMFDUpperRight] = + DisplayScalePercent("L4MFDSCALE_UR", group); + percent[SplitMFDLowerLeft] = + DisplayScalePercent("L4MFDSCALE_LL", group); + percent[SplitMFDLowerRight] = + DisplayScalePercent("L4MFDSCALE_LR", group); + percent[SplitMap] = DisplayScalePercent("L4RADARSCALE", 100); + + DEBUG_STREAM << "SVGA16: secondary displays at UL " + << percent[SplitMFDUpperLeft] << "% UC " + << percent[SplitMFDUpperCenter] << "% UR " + << percent[SplitMFDUpperRight] << "% LL " + << percent[SplitMFDLowerLeft] << "% LR " + << percent[SplitMFDLowerRight] << "% radar " + << percent[SplitMap] << "%\n" << std::flush; + } + + GlassSize glass[SplitViewCount]; + for (int view = 0; view < SplitViewCount; ++view) + { + int base_w = (view == SplitMap) ? cockpitRadarBaseW : cockpitMfdBaseW; + int base_h = (view == SplitMap) ? cockpitRadarBaseH : cockpitMfdBaseH; + glass[view].w = (base_w * percent[view]) / 100; + glass[view].h = (base_h * percent[view]) / 100; + } + + //------------------------------------------------------------------- + // Keep the pod arrangement legal in canvas units. Sizing each + // display on its own means these limits can be exact rather than one + // conservative rule covering all five: what constrains a display is + // its actual neighbour, not the largest a neighbour might have been. + // + // The panes overlap the viewscreen by design, as the pod's bezels + // did - each other, never. + //------------------------------------------------------------------- + const int canvas_w = 1920; + const int canvas_h = 1080; + const int half_w = canvas_w / 2; + + // nothing may exceed the canvas on its own + for (int view = 0; view < SplitViewCount; ++view) + { + ClampGlass(&glass[view], canvas_w, canvas_h); + } + + //------------------------------------------------------------------- + // The radar shares its column with the MFDs above and below it, so + // their heights share the canvas. Which MFDs those are, and how much + // room they leave, depends on where the radar was put. It yields, + // being the one that grows into the view. + //------------------------------------------------------------------- + RadarPlacement placement = RadarPosition(); + + if (RadarOnASide(placement)) + { + //--------------------------------------------------------------- + // Halfway up a side it has an MFD above AND below, and it grows + // from the middle in both directions - so it must clear the + // taller of the two twice over. That is a real constraint: two + // big MFDs on one side leave a centred radar very little. + //--------------------------------------------------------------- + Logical left = RadarOnTheLeft(placement); + int upper = left + ? glass[SplitMFDUpperLeft].h : glass[SplitMFDUpperRight].h; + int lower = left + ? glass[SplitMFDLowerLeft].h : glass[SplitMFDLowerRight].h; + int tallest = (upper > lower) ? upper : lower; + + ClampGlass(&glass[SplitMap], canvas_w, canvas_h - 2 * tallest); + } + else + { + //--------------------------------------------------------------- + // On the bottom edge it only has to clear the MFD hanging from + // the top of its column. + //--------------------------------------------------------------- + int above = + (placement == RadarBottomLeft) ? glass[SplitMFDUpperLeft].h : + (placement == RadarBottomRight) ? glass[SplitMFDUpperRight].h : + glass[SplitMFDUpperCenter].h; + ClampGlass(&glass[SplitMap], canvas_w, canvas_h - above); + } + + //------------------------------------------------------------------- + // The top row is a centred display with one anchored to either edge, + // so a side display gets whatever the centre one leaves of its half. + //------------------------------------------------------------------- + int top_side = half_w - glass[SplitMFDUpperCenter].w / 2; + ClampGlass(&glass[SplitMFDUpperLeft], top_side, canvas_h); + ClampGlass(&glass[SplitMFDUpperRight], top_side, canvas_h); + + //------------------------------------------------------------------- + // What the bottom row looks like depends on the radar as well: + // centred it is the middle of that row, in a corner it is a third + // pane along it, and on a side it has left the row entirely and the + // two MFDs have the whole bottom edge to share. + //------------------------------------------------------------------- + if (RadarOnASide(placement)) + { + ClampGlass(&glass[SplitMFDLowerLeft], half_w, canvas_h); + ClampGlass(&glass[SplitMFDLowerRight], half_w, canvas_h); + } + else if (placement == RadarBottomCenter) + { + int bottom_side = half_w - glass[SplitMap].w / 2; + ClampGlass(&glass[SplitMFDLowerLeft], bottom_side, canvas_h); + ClampGlass(&glass[SplitMFDLowerRight], bottom_side, canvas_h); + } + else + { + int bottom_share = (canvas_w - glass[SplitMap].w) / 2; + ClampGlass(&glass[SplitMFDLowerLeft], bottom_share, canvas_h); + ClampGlass(&glass[SplitMFDLowerRight], bottom_share, canvas_h); + } + + //------------------------------------------------------------------- + // An upper and a lower MFD share a column, anchored to opposite + // edges, so their heights must not sum past the canvas. The lower + // one yields - it is the one the radar sits beside. + //------------------------------------------------------------------- + ClampGlass(&glass[SplitMFDLowerLeft], canvas_w, + canvas_h - glass[SplitMFDUpperLeft].h); + ClampGlass(&glass[SplitMFDLowerRight], canvas_w, + canvas_h - glass[SplitMFDUpperRight].h); + + //------------------------------------------------------------------- + // Only now to device pixels: clamping in canvas units keeps every + // limit above independent of the monitor. + //------------------------------------------------------------------- + for (int view = 0; view < SplitViewCount; ++view) + { + sizes[view].w = (glass[view].w * scale) / 100; + sizes[view].h = (glass[view].h * scale) / 100; + } +} + //~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ // Fit the 1920x1080 cockpit canvas into the given client area: one // uniform scale (so the canvas never stretches - a wider desktop just @@ -3886,11 +4203,8 @@ void SWP_NOZORDER | SWP_NOACTIVATE); } - int top_glass_w = (320 * scale) / 100; - int top_glass_h = (240 * scale) / 100; - // the radar reads best big: 1.35x the compact glass - int map_glass_w = (324 * scale) / 100; - int map_glass_h = (432 * scale) / 100; + GlassSize glass[SplitViewCount]; + CockpitGlassSizes(scale, glass); for (int view = 0; view < SplitViewCount; ++view) { @@ -3898,14 +4212,7 @@ void { continue; } - if (view == SplitMap) - { - splitView[view]->Resize(map_glass_w, map_glass_h); - } - else - { - splitView[view]->Resize(top_glass_w, top_glass_h); - } + splitView[view]->Resize(glass[view].w, glass[view].h); } //--------------------------------------------------------------- @@ -3928,24 +4235,67 @@ void origin_x + view_w - splitView[SplitMFDUpperRight]->ClientWidth(), origin_y); } + //--------------------------------------------------------------- + // The radar: bottom centre, either bottom corner, or halfway up + // either side. Only when it takes a bottom corner does a lower MFD + // have to move - on a side it is out of that row's way already. + //--------------------------------------------------------------- + RadarPlacement placement = RadarPosition(); + int radar_w = (splitView[SplitMap] != NULL) + ? splitView[SplitMap]->ClientWidth() : 0; + + if (splitView[SplitMap] != NULL) + { + int radar_h = splitView[SplitMap]->ClientHeight(); + int radar_x, radar_y; + + switch (placement) + { + case RadarBottomLeft: + radar_x = origin_x; + radar_y = origin_y + view_h - radar_h; + break; + + case RadarBottomRight: + radar_x = origin_x + view_w - radar_w; + radar_y = origin_y + view_h - radar_h; + break; + + case RadarMidLeft: + radar_x = origin_x; + radar_y = origin_y + (view_h - radar_h) / 2; + break; + + case RadarMidRight: + radar_x = origin_x + view_w - radar_w; + radar_y = origin_y + (view_h - radar_h) / 2; + break; + + default: + radar_x = origin_x + (view_w - radar_w) / 2; + radar_y = origin_y + view_h - radar_h; + break; + } + + splitView[SplitMap]->SetPosition(radar_x, radar_y); + } + + int slide_left = (placement == RadarBottomLeft) ? radar_w : 0; + int slide_right = (placement == RadarBottomRight) ? radar_w : 0; + if (splitView[SplitMFDLowerLeft] != NULL) { splitView[SplitMFDLowerLeft]->SetPosition( - origin_x, + origin_x + slide_left, origin_y + view_h - splitView[SplitMFDLowerLeft]->ClientHeight()); } if (splitView[SplitMFDLowerRight] != NULL) { splitView[SplitMFDLowerRight]->SetPosition( - origin_x + view_w - splitView[SplitMFDLowerRight]->ClientWidth(), + origin_x + view_w - splitView[SplitMFDLowerRight]->ClientWidth() + - slide_right, origin_y + view_h - splitView[SplitMFDLowerRight]->ClientHeight()); } - if (splitView[SplitMap] != NULL) - { - splitView[SplitMap]->SetPosition( - origin_x + (view_w - splitView[SplitMap]->ClientWidth()) / 2, - origin_y + view_h - splitView[SplitMap]->ClientHeight()); - } // The panes go OVER the main display (pod bezel occlusion): pin // the viewscreen to the bottom of the sibling z-order so the 3D @@ -4023,35 +4373,67 @@ SVGA16::SVGA16( int work_w = work.right - work.left; int work_h = work.bottom - work.top; - int mid_x = (work_w - init_width) / 2; if (mid_x < 0) mid_x = 0; - int right_x = work_w - init_width; if (right_x < 0) right_x = 0; - int bottom_y = work_h - init_height; if (bottom_y < 0) bottom_y = 0; - + //--------------------------------------------------------------- + // Each display carries its own button bank here too, at native + // size: the banks reach under the glass with their indicator + // strips clearing the edge, exactly as in the composited + // cockpit - so the buttons can be read and pressed full size. + //--------------------------------------------------------------- splitView[SplitMFDUpperLeft] = new MFDSplitView( "MFD upper left", init_width, init_height, init_width, init_height, work.left, work.top, - MFDSplitView::NoButtons, 0, 0, 0); + MFDSplitView::MFDStrips, 0x2F, 0, 0); splitView[SplitMFDUpperCenter] = new MFDSplitView( "MFD upper center", init_width, init_height, - init_width, init_height, work.left + mid_x, work.top, - MFDSplitView::NoButtons, 0, 0, 0); + init_width, init_height, work.left, work.top, + MFDSplitView::MFDStrips, 0x27, 0, 0); splitView[SplitMFDUpperRight] = new MFDSplitView( "MFD upper right", init_width, init_height, - init_width, init_height, work.left + right_x, work.top, - MFDSplitView::NoButtons, 0, 0, 0); + init_width, init_height, work.left, work.top, + MFDSplitView::MFDStrips, 0x37, 0, 0); splitView[SplitMFDLowerLeft] = new MFDSplitView( "MFD lower left", init_width, init_height, - init_width, init_height, work.left, work.top + bottom_y, - MFDSplitView::NoButtons, 0, 0, 0); + init_width, init_height, work.left, work.top, + MFDSplitView::MFDStrips, 0x0F, 0, 0); splitView[SplitMFDLowerRight] = new MFDSplitView( "MFD lower right", init_width, init_height, - init_width, init_height, work.left + right_x, work.top + bottom_y, - MFDSplitView::NoButtons, 0, 0, 0); + init_width, init_height, work.left, work.top, + MFDSplitView::MFDStrips, 0x07, 0, 0); // map is portrait-mounted: source rotated to 480x640 splitView[SplitMap] = new MFDSplitView( "Map", init_height, init_width, - init_height, init_width, work.left + mid_x, work.top + bottom_y, - MFDSplitView::NoButtons, 0, 0, 0); + init_height, init_width, work.left, work.top, + MFDSplitView::SideColumns, 0x10, 0x18, 0); + + //--------------------------------------------------------------- + // Lay them out in the pod arrangement from their measured sizes + // (the banks make each window bigger than its glass). + //--------------------------------------------------------------- + RECT probe; + probe.left = 0; probe.top = 0; probe.right = 100; probe.bottom = 100; + AdjustWindowRect(&probe, + WS_OVERLAPPED | WS_CAPTION | WS_SYSMENU | WS_MINIMIZEBOX, FALSE); + int chrome_w = (probe.right - probe.left) - 100; + int chrome_h = (probe.bottom - probe.top) - 100; + + int mfd_w = splitView[SplitMFDUpperLeft]->ClientWidth() + chrome_w; + int mfd_h = splitView[SplitMFDUpperLeft]->ClientHeight() + chrome_h; + int map_w = splitView[SplitMap]->ClientWidth() + chrome_w; + int map_h = splitView[SplitMap]->ClientHeight() + chrome_h; + + int mid_x = (work_w - mfd_w) / 2; if (mid_x < 0) mid_x = 0; + int right_x = work_w - mfd_w; if (right_x < 0) right_x = 0; + int bottom_y = work_h - mfd_h; if (bottom_y < 0) bottom_y = 0; + int map_x = (work_w - map_w) / 2; if (map_x < 0) map_x = 0; + int map_y = work_h - map_h; if (map_y < 0) map_y = 0; + + splitView[SplitMFDUpperLeft]->SetPosition(work.left, work.top); + splitView[SplitMFDUpperCenter]->SetPosition(work.left + mid_x, work.top); + splitView[SplitMFDUpperRight]->SetPosition(work.left + right_x, work.top); + splitView[SplitMFDLowerLeft]->SetPosition(work.left, work.top + bottom_y); + splitView[SplitMFDLowerRight]->SetPosition( + work.left + right_x, work.top + bottom_y); + splitView[SplitMap]->SetPosition(work.left + map_x, work.top + map_y); } else if (splitViews) { @@ -4073,8 +4455,20 @@ SVGA16::SVGA16( HWND cockpit = ApplicationManager::GetCurrentManager()->GetHWnd(); + //--------------------------------------------------------------- + // -fit takes the whole monitor rather than the work area, and + // pays no chrome: the shell goes borderless below. Otherwise + // stay inside the work area so the taskbar is still reachable. + //--------------------------------------------------------------- + bool fit_display = L4Application::GetFitDisplay(); + int chrome_w = 16, chrome_h = 40; - if (cockpit != NULL) + if (fit_display) + { + chrome_w = 0; + chrome_h = 0; + } + else if (cockpit != NULL) { RECT outer, inner; GetWindowRect(cockpit, &outer); @@ -4085,7 +4479,28 @@ SVGA16::SVGA16( RECT work; work.left = 0; work.top = 0; work.right = 1920; work.bottom = 1080; - SystemParametersInfoA(SPI_GETWORKAREA, 0, &work, 0); + if (fit_display) + { + MONITORINFO monitor; + memset(&monitor, 0, sizeof(monitor)); + monitor.cbSize = sizeof(monitor); + HMONITOR handle = MonitorFromWindow( + (cockpit != NULL) ? cockpit : GetDesktopWindow(), + MONITOR_DEFAULTTOPRIMARY); + if (GetMonitorInfoA(handle, &monitor)) + { + work = monitor.rcMonitor; + } + else + { + work.right = GetSystemMetrics(SM_CXSCREEN); + work.bottom = GetSystemMetrics(SM_CYSCREEN); + } + } + else + { + SystemParametersInfoA(SPI_GETWORKAREA, 0, &work, 0); + } int avail_w = (work.right - work.left) - chrome_w; int avail_h = (work.bottom - work.top) - chrome_h; @@ -4115,12 +4530,35 @@ SVGA16::SVGA16( //--------------------------------------------------------------- if (cockpit != NULL) { - SetWindowLongPtrA(cockpit, GWL_STYLE, - GetWindowLongPtrA(cockpit, GWL_STYLE) | WS_CLIPCHILDREN); + LONG_PTR style = GetWindowLongPtrA(cockpit, GWL_STYLE); - SetWindowPos(cockpit, NULL, work.left, work.top, - client_w + chrome_w, client_h + chrome_h, - SWP_NOZORDER | SWP_NOACTIVATE | SWP_FRAMECHANGED); + if (fit_display) + { + //------------------------------------------------------- + // Borderless over the whole monitor. The window is the + // full panel, wider than 16:9 or not - LayoutCockpit + // centres the canvas inside it and the leftover stays + // black, so this letterboxes exactly like a resize. + //------------------------------------------------------- + style &= ~(WS_CAPTION | WS_THICKFRAME | WS_SYSMENU | + WS_MINIMIZEBOX | WS_MAXIMIZEBOX | WS_BORDER | + WS_DLGFRAME); + style |= WS_POPUP | WS_CLIPCHILDREN; + SetWindowLongPtrA(cockpit, GWL_STYLE, style); + + SetWindowPos(cockpit, NULL, work.left, work.top, + work.right - work.left, work.bottom - work.top, + SWP_NOZORDER | SWP_NOACTIVATE | SWP_FRAMECHANGED); + } + else + { + SetWindowLongPtrA(cockpit, GWL_STYLE, + style | WS_CLIPCHILDREN); + + SetWindowPos(cockpit, NULL, work.left, work.top, + client_w + chrome_w, client_h + chrome_h, + SWP_NOZORDER | SWP_NOACTIVATE | SWP_FRAMECHANGED); + } } //--------------------------------------------------------------- @@ -4161,40 +4599,34 @@ SVGA16::SVGA16( // 0x37.. / lower left 0x0F.. / lower right 0x07.. and the map // flanked by Secondary 0x10-0x15 / Screen 0x18-0x1D. //--------------------------------------------------------------- - // all five MFDs at the compact size, corners + top center - int top_glass_w = COCKPIT_CANVAS(320); - int top_glass_h = COCKPIT_CANVAS(240); - int bot_glass_w = COCKPIT_CANVAS(320); - int bot_glass_h = COCKPIT_CANVAS(240); - // the radar reads best big: 1.35x the compact glass - // (1.5x proved a touch too large in playtest) - int map_glass_w = COCKPIT_CANVAS(324); - int map_glass_h = COCKPIT_CANVAS(432); + // each display at whatever the player asked for it + GlassSize glass[SplitViewCount]; + CockpitGlassSizes(scale_num, glass); splitView[SplitMFDUpperLeft] = new MFDSplitView( "MFD upper left", init_width, init_height, - top_glass_w, top_glass_h, 0, 0, + glass[SplitMFDUpperLeft].w, glass[SplitMFDUpperLeft].h, 0, 0, MFDSplitView::MFDStrips, 0x2F, 0, cockpit); splitView[SplitMFDUpperCenter] = new MFDSplitView( "MFD upper center", init_width, init_height, - top_glass_w, top_glass_h, 0, 0, + glass[SplitMFDUpperCenter].w, glass[SplitMFDUpperCenter].h, 0, 0, MFDSplitView::MFDStrips, 0x27, 0, cockpit); splitView[SplitMFDUpperRight] = new MFDSplitView( "MFD upper right", init_width, init_height, - top_glass_w, top_glass_h, 0, 0, + glass[SplitMFDUpperRight].w, glass[SplitMFDUpperRight].h, 0, 0, MFDSplitView::MFDStrips, 0x37, 0, cockpit); splitView[SplitMFDLowerLeft] = new MFDSplitView( "MFD lower left", init_width, init_height, - bot_glass_w, bot_glass_h, 0, 0, + glass[SplitMFDLowerLeft].w, glass[SplitMFDLowerLeft].h, 0, 0, MFDSplitView::MFDStrips, 0x0F, 0, cockpit); splitView[SplitMFDLowerRight] = new MFDSplitView( "MFD lower right", init_width, init_height, - bot_glass_w, bot_glass_h, 0, 0, + glass[SplitMFDLowerRight].w, glass[SplitMFDLowerRight].h, 0, 0, MFDSplitView::MFDStrips, 0x07, 0, cockpit); // map is portrait: source rotated 90 degrees clockwise splitView[SplitMap] = new MFDSplitView( "Map", init_height, init_width, - map_glass_w, map_glass_h, 0, 0, + glass[SplitMap].w, glass[SplitMap].h, 0, 0, MFDSplitView::SideColumns, 0x10, 0x18, cockpit); //--------------------------------------------------------------- diff --git a/pack-dist.ps1 b/pack-dist.ps1 index 332fa1c..c188aea 100644 --- a/pack-dist.ps1 +++ b/pack-dist.ps1 @@ -144,6 +144,56 @@ L4PLASMA=SCREEN # 480x640 - decoded exactly as the pod's VDB split them, no downscale). L4MFDSPLIT=1 +# Size of the six secondary displays in the glass cockpit, as a +# percentage of their pod size. The pod bolted them down at one size; +# on a big panel there is room to trade viewscreen for instrument, so +# turn these up if you want to actually read the other displays while +# you fly. 100 = as the pod had them. Range 25-200 (out-of-range and +# unreadable values fall back to the group setting, then to 100). +# +# The scaling is applied in canvas units, before the cockpit is fitted +# to your window, so a given number looks the same on every monitor. +# The layout stays legal whatever you ask for - the panes are clamped +# against their actual neighbours, shrinking uniformly so a display +# never comes out stretched. They do overlap the viewscreen, exactly +# as the pod's bezels did, but never each other. +# +# L4MFDSCALE sets all five green MFDs at once. +L4MFDSCALE=100 + +# ...and any single display can override it. Uncomment one to size it +# on its own - useful if you only care about, say, the damage readout. +# UL upper left UC upper center UR upper right +# LL lower left LR lower right +#L4MFDSCALE_UL=100 +#L4MFDSCALE_UC=100 +#L4MFDSCALE_UR=100 +#L4MFDSCALE_LL=100 +#L4MFDSCALE_LR=100 + +# The portrait radar/map, sized on its own (it already sits at 1.35x +# the MFDs by default). It shares the canvas with whichever MFD is +# above it, so at extreme settings one of the two gives way. +L4RADARSCALE=100 + +# Where the radar sits: +# CENTER bottom centre, under the viewscreen, as the pod had it +# (default; BOTTOM and CENTRE mean the same) +# LEFT bottom left corner (or BOTTOMLEFT) +# RIGHT bottom right corner (or BOTTOMRIGHT) +# MIDLEFT left edge, halfway up (or LEFTCENTER / LEFTCENTRE) +# MIDRIGHT right edge, halfway up (or RIGHTCENTER / RIGHTCENTRE) +# Anywhere but CENTER stops it blocking the middle of the road, which +# is worth having on a wide screen. +# +# In a bottom corner it is one of three panes along the bottom, and the +# lower MFD whose corner it takes slides inboard beside it. Halfway up +# a side it leaves the bottom row entirely and sits between that side's +# two MFDs - roomy on a tall radar, but if the MFDs on that side are +# also scaled up, the radar is the one that gives way (it has to clear +# both of them, and it grows from the middle in both directions). +L4RADARPOS=CENTER + # Simulation/render frame rate, integer frames/second. The desktop # default is 60; the arcade pods shipped at 25. TARGETFPS=60 @@ -243,12 +293,24 @@ cd /d "%~dp0" start rpl4opt.exe -windowed -res 1920 1080 "@ +Set-Content -Path "$dist\start-fullscreen.bat" -Encoding ascii -Value @" +@echo off +rem Red Planet 4.12 - borderless over the whole monitor. -fit sizes the +rem window to the panel AND picks the render size to match the cockpit +rem canvas it lands in, so no -res guessing: the 3D arrives 1:1 instead +rem of being stretched. Pass -res yourself to override it. +rem (Add -egg TEST.EGG to skip the menu and run the canned mission.) +cd /d "%~dp0" +start rpl4opt.exe -fit +"@ + Set-Content -Path "$dist\README.txt" -Encoding ascii -Value @" Red Planet 4.12.2 ================= -Run start-windowed.bat (or: rpl4opt.exe -windowed -res 800 600 -egg TEST.EGG). -No cockpit hardware needed. If there is no sound, run oalinst.exe once. +Run start-fullscreen.bat for borderless over the whole monitor, or +start-windowed.bat to keep a title bar. No cockpit hardware needed. +If there is no sound, run oalinst.exe once. Controls (XInput controller and/or keyboard) - EVERY input is rebindable: edit bindings.txt beside the exe (written with the full @@ -268,8 +330,19 @@ documented default layout on first run; delete it to restore). The cockpit scales to whatever window you give it - maximise it and it grows, keeping its 16:9 shape (an ultrawide gets black bars rather than -a stretched cockpit). The 3D itself renders at whatever -res says, so -on a big screen raise it to match: rpl4opt.exe -windowed -res 2560 1440. +a stretched cockpit). + +The 3D itself renders at whatever -res says and is then stretched onto +the cockpit's viewscreen, so a mismatched -res costs sharpness. -fit +takes care of that for you: it measures the monitor, works out the +canvas the cockpit will settle on, and asks for exactly that render +size, so the picture arrives 1:1. + + rpl4opt.exe -fit borderless, res chosen for you + rpl4opt.exe -fit -res 1280 720 same window, lighter render + rpl4opt.exe -windowed -res 2560 1440 pick both yourself + +(-windowed-fullscreen is accepted as a long spelling of -fit.) The full pod cockpit comes up in a single window: three green MFDs across the top, the 3D viewscreen centered with the orange plasma glass