diff --git a/MUNGA_L4/L4APP.H b/MUNGA_L4/L4APP.H index d01c3e2..57bf11f 100644 --- a/MUNGA_L4/L4APP.H +++ b/MUNGA_L4/L4APP.H @@ -82,6 +82,10 @@ public: // -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; } + // -fit's borderless full-monitor placement. Applied once at startup so + // the window is in its final shape before ANY mission builds a device + // against it - see the definition for why the first race differed. + static void FitWindowToMonitor(HWND window); static Logical GetSeeSolids() { return seeSolids; } static unsigned long GetNetworkCommonFlatAddress() { return networkCommonFlatAddress; } // The front end's multiplayer path turns network mode on at launch diff --git a/MUNGA_L4/L4APP.cpp b/MUNGA_L4/L4APP.cpp index fd4698c..fabaffd 100644 --- a/MUNGA_L4/L4APP.cpp +++ b/MUNGA_L4/L4APP.cpp @@ -302,6 +302,72 @@ void << monitor_w << "x" << monitor_h << " monitor\n" << std::flush; } +// +//############################################################################# +// FitWindowToMonitor +//############################################################################# +// +// -fit's borderless full-monitor placement, applied to the shell window. +// +// This has to happen BEFORE the first race, not during it. SVGA16 does the +// same thing when it assembles the cockpit, but that is not until a mission +// starts - and the D3D device is created just ahead of it, against whatever +// the window is at that moment. So the first race got a device sized to a +// still-bordered window and every race after it got one sized to the +// borderless monitor: two different render targets, two different frame +// costs, from one lobby and one set of settings. +// +// A racing sim cannot have that. The window reaches its final shape while +// the front end is still up, so every mission of a session - the first one +// included - is set up against exactly the same client area. +// +// SVGA16 still applies it when it builds the cockpit. That call becomes a +// no-op rather than a change, which is the point. +// +void + L4Application::FitWindowToMonitor(HWND window) +{ + if (window == NULL) + { + return; + } + + RECT monitor_rect; + monitor_rect.left = 0; + monitor_rect.top = 0; + monitor_rect.right = GetSystemMetrics(SM_CXSCREEN); + monitor_rect.bottom = GetSystemMetrics(SM_CYSCREEN); + + MONITORINFO monitor; + memset(&monitor, 0, sizeof(monitor)); + monitor.cbSize = sizeof(monitor); + HMONITOR handle = MonitorFromWindow(window, MONITOR_DEFAULTTOPRIMARY); + if (GetMonitorInfoA(handle, &monitor)) + { + monitor_rect = monitor.rcMonitor; + } + + // + // Same style surgery SVGA16 performs, so the two agree exactly. + // + LONG_PTR style = GetWindowLongPtrA(window, GWL_STYLE); + style &= ~(WS_CAPTION | WS_THICKFRAME | WS_SYSMENU | + WS_MINIMIZEBOX | WS_MAXIMIZEBOX | WS_BORDER | WS_DLGFRAME); + style |= WS_POPUP | WS_CLIPCHILDREN; + SetWindowLongPtrA(window, GWL_STYLE, style); + + SetWindowPos(window, NULL, + monitor_rect.left, monitor_rect.top, + monitor_rect.right - monitor_rect.left, + monitor_rect.bottom - monitor_rect.top, + SWP_NOZORDER | SWP_NOACTIVATE | SWP_FRAMECHANGED); + + DEBUG_STREAM << "L4Application: -fit placed the window borderless at " + << (monitor_rect.right - monitor_rect.left) << "x" + << (monitor_rect.bottom - monitor_rect.top) + << " before the first mission\n" << std::flush; +} + // //############################################################################# // ParseCommandLine diff --git a/MUNGA_L4/L4VIDEO.cpp b/MUNGA_L4/L4VIDEO.cpp index 8c07e6f..94aed2e 100644 --- a/MUNGA_L4/L4VIDEO.cpp +++ b/MUNGA_L4/L4VIDEO.cpp @@ -1695,11 +1695,28 @@ DPLRenderer::DPLRenderer( mPresentParams.EnableAutoDepthStencil = TRUE; mPresentParams.AutoDepthStencilFormat = D3DFMT_D24X8; mPresentParams.Windowed = !fullscreen; - if (fullscreen) - { - mPresentParams.BackBufferWidth = screenWidth; - mPresentParams.BackBufferHeight = screenHeight; - } + // + // The render size is asked for, not suggested - windowed as well as + // full-screen. Left at zero, D3D sizes the back buffer to the device + // window's client area AT THIS MOMENT, and everything downstream is + // built from the size we asked for instead: the projection matrix + // takes its aspect from it, and the reticle is centred on it. A + // window that is not exactly that size therefore renders at the + // wrong shape and gets rescaled on the way to the viewscreen pane. + // + // It only showed up on a SECOND race. A fresh renderer is built per + // mission while the window carries its cockpit placement across, so + // the first race creates its device against a still-bordered window + // - near enough the asked-for size to pass - and the next one + // against the borderless full-monitor client, which on a 3440x1440 + // panel meant a 1.778 image drawn across a 2.389 target. + // + // -fit picks a render size to land on the viewscreen 1:1, so honour + // it: the back buffer is that size, and Present scales it to the + // pane in one uniform step. + // + mPresentParams.BackBufferWidth = screenWidth; + mPresentParams.BackBufferHeight = screenHeight; HRESULT hr; diff --git a/MUNGA_L4/L4VIDRND.cpp b/MUNGA_L4/L4VIDRND.cpp index ffc8350..e183145 100644 --- a/MUNGA_L4/L4VIDRND.cpp +++ b/MUNGA_L4/L4VIDRND.cpp @@ -2364,7 +2364,76 @@ ReticleRenderable::ReticleRenderable( LPDIRECT3DDEVICE9 device = myRenderer->GetDevice(); - device->CreateVertexBuffer(sizeof(L4VERTEX_2D) * 8, D3DUSAGE_WRITEONLY, L4VERTEX_2D_FVF, D3DPOOL_MANAGED, &mVB, NULL); + device->CreateVertexBuffer(sizeof(L4VERTEX_2D) * crosshairVertexCount, D3DUSAGE_WRITEONLY, L4VERTEX_2D_FVF, D3DPOOL_MANAGED, &mVB, NULL); + + // + // Nothing is written here. The crosshair is measured against the + // render target it will actually be drawn into, and that is not + // known to be the renderer's requested size - see RebuildCrosshair. + // + mBuiltWidth = 0.0f; + mBuiltHeight = 0.0f; + mBuiltOriginX = -1.0f; + mBuiltOriginY = -1.0f; + RebuildCrosshair(); +} + +//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +// Centre the crosshair on the target it is about to be drawn into. +// +// It used to be baked once, in the constructor, from the renderer's +// GetWidth()/GetHeight() - the size the renderer ASKED for. A windowed +// device does not necessarily get it: BackBufferWidth/Height are only +// filled in for fullscreen (L4VIDEO.cpp), so windowed the back buffer is +// whatever the device window's client area happened to be when the +// device was created. A fresh renderer is built per mission while the +// window carries its restored cockpit placement across, so the two agree +// on the first race and can disagree on the next one - which put the +// crosshair off-centre by half the difference, and only ever on a second +// game. +// +// Measuring the viewport at draw time settles it for every case at once: +// the windowed mismatch, a device reset, and the podium's pillarbox crop. +// +void ReticleRenderable::RebuildCrosshair() +{ + Check(this); + + if (mVB == NULL) + { + return; + } + LPDIRECT3DDEVICE9 device = myRenderer->GetDevice(); + if (device == NULL) + { + return; + } + + D3DVIEWPORT9 viewport; + if (FAILED(device->GetViewport(&viewport)) || + viewport.Width == 0 || viewport.Height == 0) + { + return; + } + + float width = (float) viewport.Width; + float height = (float) viewport.Height; + // + // Origin included: a viewport that moves without resizing still + // carries the crosshair with it. + // + float origin_x = (float) viewport.X; + float origin_y = (float) viewport.Y; + if (width == mBuiltWidth && height == mBuiltHeight && + origin_x == mBuiltOriginX && origin_y == mBuiltOriginY) + { + return; + } + + mBuiltWidth = width; + mBuiltHeight = height; + mBuiltOriginX = origin_x; + mBuiltOriginY = origin_y; L4VERTEX_2D *verts; mVB->Lock(0, 0, (void**)&verts, 0); @@ -2372,62 +2441,125 @@ ReticleRenderable::ReticleRenderable( DWORD color = D3DCOLOR_XRGB(0, 128, 0); float segmentLen = 5.0f / 192.0f; float spread = 5.0f / 256.0f; - float width = myRenderer->GetWidth(); - float height = myRenderer->GetHeight(); - float centerX = width / 2.0f; - float centerY = height / 2.0f; + // + // Pre-transformed vertices are absolute screen pixels - the viewport + // clips them but does not shift them - so its origin is carried here. + // + float centerX = (float) viewport.X + width / 2.0f; + float centerY = (float) viewport.Y + height / 2.0f; - // top segment - verts[0].x = centerX; - verts[0].y = centerY - (spread + segmentLen) * height; - verts[0].z = 0.0f; - verts[0].rhw = 1.0f; - verts[0].color = color; + // + // Land on the pixel CENTRE, not the corner, so the quads below span + // whole pixels: an arm one pixel wide about a centre of x.5 runs from + // x.0 to x+1.0 and covers exactly one column. + // + centerX = (float)(int) centerX + 0.5f; + centerY = (float)(int) centerY + 0.5f; - verts[1].x = centerX; - verts[1].y = centerY - spread * height; - verts[1].z = 0.0f; - verts[1].rhw = 1.0f; - verts[1].color = color; + // + // Worth a line in the log: it names the target, the renderer's + // requested size and where the crosshair actually landed, so a + // report of it being off says which of the three moved. + // + DEBUG_STREAM << "Reticle: centred at " << centerX << "," << centerY + << " on the " << viewport.Width << "x" << viewport.Height + << " viewport at " << viewport.X << "," << viewport.Y + << " (renderer asked for " << myRenderer->GetWidth() << "x" + << myRenderer->GetHeight() << ")"; + if (viewport.Width != myRenderer->GetWidth() || + viewport.Height != myRenderer->GetHeight()) + { + // + // Not the crosshair's problem alone: the projection matrix is + // built from the requested size too, so a target this does not + // match is being rendered at the wrong aspect and rescaled on + // the way to the pane. + // + DEBUG_STREAM << " - TARGET DISAGREES, aspect " + << ((float) viewport.Width / (float) viewport.Height) + << " drawn as " + << ((float) myRenderer->GetWidth() / (float) myRenderer->GetHeight()); + } + DEBUG_STREAM << "\n" << std::flush; - // right segment - verts[2].x = centerX + (spread + segmentLen) * height; - verts[2].y = centerY; - verts[2].z = 0.0f; - verts[2].rhw = 1.0f; - verts[2].color = color; + // + // Each arm is a quad one pixel thick rather than a line. D3D9 line + // rasterisation follows the diamond-exit rule and is free to differ + // between drivers on a segment that runs along a pixel boundary, + // which is how a crosshair loses one pair of arms and keeps the + // other. Triangles have a fill rule that does not vary, so an arm + // spanning whole pixels lands the same way everywhere - and on a + // full-screen device, where both viewport dimensions are usually + // even and BOTH pairs sit on boundaries, that is the difference + // between a crosshair and nothing at all. + // + float inner = spread * height; + float outer = (spread + segmentLen) * height; - verts[3].x = centerX + spread * height; - verts[3].y = centerY; - verts[3].z = 0.0f; - verts[3].rhw = 1.0f; - verts[3].color = color; + // + // Thickness follows the target the way the arms' length does, rather + // than being one pixel whatever the resolution. + // + // One pixel is a width the PRESENTATION can lose. The scene goes to + // the viewscreen pane through a stretch, and when the back buffer is + // wider than the pane that stretch samples straight past a feature a + // single pixel across. A second race rendering 3440 wide into the + // 2553-wide pane is 0.74 across and 1.00 down, which took the + // vertical arms and left the horizontal ones standing - the crosshair + // was being drawn correctly and thrown away on the way to the glass. + // + // Scaling it also keeps the pod's proportions: one pixel at the 480 + // lines this was drawn for is three at 1440, not a hairline. + // + float thickness = (float)(int)(height / 480.0f); + if (thickness < 1.0f) + { + thickness = 1.0f; + } + float half = thickness * 0.5f; - // bottom segment - verts[4].x = centerX; - verts[4].y = centerY + (spread + segmentLen) * height; - verts[4].z = 0.0f; - verts[4].rhw = 1.0f; - verts[4].color = color; + struct Arm + { + float x0, y0, x1, y1; // opposite corners, in pixels + }; + const Arm arms[4] = + { + // top + { centerX - half, centerY - outer, centerX + half, centerY - inner }, + // right + { centerX + inner, centerY - half, centerX + outer, centerY + half }, + // bottom + { centerX - half, centerY + inner, centerX + half, centerY + outer }, + // left + { centerX - outer, centerY - half, centerX - inner, centerY + half } + }; - verts[5].x = centerX; - verts[5].y = centerY + spread * height; - verts[5].z = 0.0f; - verts[5].rhw = 1.0f; - verts[5].color = color; + int v = 0; + for (int a = 0; a < 4; ++a) + { + const Arm &arm = arms[a]; + // + // Two triangles, corners in the order top-left, top-right, + // bottom-left / top-right, bottom-right, bottom-left. Culling is + // turned off for the draw, so the winding does not have to agree + // with the renderer's global cull mode. + // + const float quad_x[6] = + { arm.x0, arm.x1, arm.x0, arm.x1, arm.x1, arm.x0 }; + const float quad_y[6] = + { arm.y0, arm.y0, arm.y1, arm.y0, arm.y1, arm.y1 }; - // left segment - verts[6].x = centerX - (spread + segmentLen) * height; - verts[6].y = centerY; - verts[6].z = 0.0f; - verts[6].rhw = 1.0f; - verts[6].color = color; - - verts[7].x = centerX - spread * height; - verts[7].y = centerY; - verts[7].z = 0.0f; - verts[7].rhw = 1.0f; - verts[7].color = color; + for (int c = 0; c < 6; ++c) + { + verts[v].x = quad_x[c]; + verts[v].y = quad_y[c]; + verts[v].z = 0.0f; + verts[v].rhw = 1.0f; + verts[v].color = color; + ++v; + } + } + Verify(v == crosshairVertexCount); mVB->Unlock(); } @@ -2497,9 +2629,26 @@ void ReticleRenderable::Render(int pass, const D3DXMATRIX *viewTransform) { LPDIRECT3DDEVICE9 device = myRenderer->GetDevice(); + // + // The target is whatever is bound right now, so ask it now. A + // compare against the size already built means this costs one + // GetViewport a frame and rewrites nothing until it moves. + // + RebuildCrosshair(); + + // + // The arms are quads, and which way round they wind is not worth + // making the renderer's global cull mode responsible for. + // + DWORD cull_mode; + device->GetRenderState(D3DRS_CULLMODE, &cull_mode); + device->SetRenderState(D3DRS_CULLMODE, D3DCULL_NONE); + device->SetTexture(0, NULL); device->SetStreamSource(0, mVB, 0, sizeof(L4VERTEX_2D)); - device->DrawPrimitive(D3DPT_LINELIST, 0, 4); + device->DrawPrimitive(D3DPT_TRIANGLELIST, 0, 8); + + device->SetRenderState(D3DRS_CULLMODE, cull_mode); } } //~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ diff --git a/MUNGA_L4/L4VIDRND.h b/MUNGA_L4/L4VIDRND.h index a054b7f..61a0fc8 100644 --- a/MUNGA_L4/L4VIDRND.h +++ b/MUNGA_L4/L4VIDRND.h @@ -783,6 +783,19 @@ class ReticleRenderable : void Render(int pass, const D3DXMATRIX *viewTransform); protected: + // + // The crosshair is written as pre-transformed vertices - screen + // PIXELS - so it is only centred for the target it was measured + // against. Re-measure before drawing and rebuild when that + // target changes, rather than baking it once at construction. + // + void RebuildCrosshair(); + + // four arms, two triangles each + enum { crosshairVertexCount = 24 }; + + // viewport the vertex buffer currently describes + float mBuiltWidth, mBuiltHeight, mBuiltOriginX, mBuiltOriginY; // Last known position of the reticle Vector2DOf myOldReticlePosition; diff --git a/RP_L4/RPL4.CPP b/RP_L4/RPL4.CPP index fe8c8c4..7fe3c33 100644 --- a/RP_L4/RPL4.CPP +++ b/RP_L4/RPL4.CPP @@ -342,12 +342,41 @@ int WINAPI WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, LPSTR lpCmdLine } - DWORD wsStyle = WS_OVERLAPPED | WS_SYSMENU; - if (L4Application::GetFullscreen()) - wsStyle = WS_POPUP; - hWnd = CreateWindowEx(0, L"MainWndClass", L"RPL4", WS_OVERLAPPEDWINDOW, 0, 0, L4Application::GetScreenWidth(), L4Application::GetScreenHeight(), (HWND)NULL, (HMENU)NULL, hInstance, (LPVOID)NULL); - if (!hWnd) - return FALSE; + // + // The style the window is BORN with, rather than one worked out and + // then thrown away - the old code computed a style and handed + // CreateWindowEx a literal WS_OVERLAPPEDWINDOW regardless. + // + // Windowed keeps the full overlapped set on purpose: the sizing + // border and the maximise box are how the player arranges the + // cockpit, and mfd_layout.cfg remembers where they left it. The + // borderless modes are born borderless instead of being restyled a + // moment later, so there is no framed window on screen first. + // + DWORD wsStyle = WS_OVERLAPPEDWINDOW; + if (L4Application::GetFullscreen() || L4Application::GetFitDisplay()) + { + wsStyle = WS_POPUP; + } + + // + // -res is a RENDER size, so it is the client area that has to be it. + // Passed straight to CreateWindowEx it sets the OUTER rectangle and + // the chrome comes out of the middle - which is how -res 640 480 + // came to present into a 624x441 client. + // + RECT wanted; + wanted.left = 0; + wanted.top = 0; + wanted.right = (LONG) L4Application::GetScreenWidth(); + wanted.bottom = (LONG) L4Application::GetScreenHeight(); + AdjustWindowRect(&wanted, wsStyle, FALSE); + + hWnd = CreateWindowEx(0, L"MainWndClass", L"RPL4", wsStyle, 0, 0, + wanted.right - wanted.left, wanted.bottom - wanted.top, + (HWND)NULL, (HMENU)NULL, hInstance, (LPVOID)NULL); + if (!hWnd) + return FALSE; ShowWindow(hWnd, nShowCmd); @@ -364,6 +393,23 @@ int WINAPI WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, LPSTR lpCmdLine RPWindowLayout_Register(hWnd, "RPL4", True); RPWindowLayout_Load(); } + else if (L4Application::GetFitDisplay()) + { + // + // -fit has no saved placement to restore, but it does have a + // shape to take, and it has to take it NOW. SVGA16 applies the + // same borderless full-monitor rect when it assembles the + // cockpit, which is after the first mission has already built + // its D3D device against the window as it stands - so the first + // race of a session used to render to a bordered client and + // every race after it to the borderless monitor. Same lobby, + // same settings, different target and different frame cost. + // + // Settle the window here and every mission of the session, + // first included, is set up against the identical client area. + // + L4Application::FitWindowToMonitor(hWnd); + } // //-------------------------------------------------------------------------