The render target is the size that was asked for
A crosshair off-centre on the second race, and underneath it every race after the first was a different race. Windowed, BackBufferWidth/Height were left at zero, so D3D sized the back buffer to the device window's client area at the moment the device was created. Everything downstream is built from the size we ASKED for instead - the projection matrix takes its aspect from it, the reticle is centred on it - so a window that was not exactly that size rendered at the wrong shape and got rescaled on the way to the viewscreen pane. -fit decided which window that was, and it decided differently for the first mission than for the rest. Its borderless full-monitor placement lived only in SVGA16's cockpit build, which does not run until a mission starts - just after that mission has built its device. So race one was set up against a still-bordered client and every race after it against the borderless monitor. On a 3440x1440 panel that is a 1.778 image drawn across a 2.389 target, against 1.816 the first time. That is not a cosmetic difference. The simulation advances on wall-clock deltas, so frame cost is physics: two render targets that size and scale differently are two different races from one lobby and one set of settings. A racing sim does not get to do that. So: the back buffer is the requested size windowed as well as full-screen, and -fit takes its shape at startup rather than four screens later. SVGA16 still applies the same rect when it builds the cockpit - that call is now a no-op instead of a change, which is the point. The first lobby also stops being the only one with a title bar. The reticle keeps its own share of the blame and is fixed on its own terms, so it cannot drift again if a target ever does move: - It is measured against the viewport at draw time and rebuilt when that changes, rather than baked once in the constructor from the renderer's requested size. One GetViewport a frame, no rewrite until it moves. - The arms are quads, not lines. D3D9 line rasterisation follows the diamond-exit rule and is free to differ between drivers on a segment running along a pixel boundary, which is how a crosshair loses one pair of arms and keeps the other - and full-screen, where both dimensions are usually even and both pairs sit on boundaries, how it can lose the lot. - Arm thickness follows the target rather than being one pixel whatever the resolution. One pixel is a width the presentation can throw away in a downscale, and it was a hairline at 1440 next to the pod's line at 480. The log names the viewport, the requested size and where the crosshair landed, and says TARGET DISAGREES with both aspects when the first two do not match - so the next report of this arrives with its own diagnosis. Window creation cleaned up while in there: it computed a style and then handed CreateWindowEx a literal WS_OVERLAPPEDWINDOW regardless, so the full-screen path never got the WS_POPUP it thought it was asking for. Borderless modes are now born borderless instead of being restyled a moment after. The requested size also goes through AdjustWindowRect, because -res is a render size and was being used as the OUTER rectangle with the chrome taken out of the middle - which is how -res 640 480 came to present into a 624x441 client and started all of this. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
+200
-51
@@ -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);
|
||||
}
|
||||
}
|
||||
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
Reference in New Issue
Block a user