Advertisement

Drawing point seems to lag behind...

Started by July 21, 2017 04:05 AM
11 comments, last by MarcusAseth 7 years, 4 months ago

I have this Direct2D framework set up, it render a constant 60FPS and this is my code in my draw call:


	mRenderTarger->Clear(D2D1::ColorF(0.41f, 0.41f, 0.41f, 1.0f));

	GetCursorPos(&mMouse);
	ScreenToClient(mhWindow, &mMouse);
	D2D1_POINT_2F point2 = D2D1::Point2F(mMouse.x, mMouse.y);
	
	//wrapper function of the Direct2D FillEllipse function
	drawPoint(point2, 6.f, D2D1::ColorF(1.0f, 0.15f, 0.30f, 1.f));

Now even without moving the mouse too fase, the point drawn kind of lag behind a bit, and moving the mouse faster, the point is drawn as far as 5cm from my mouse cursor so the lag is visible.

Any idea why this accurs and how to fix it? 

I'm not an expert in this, but it's common for the mouse cursor to be handled by the display drivers as a special case, rather than explicitly rendered onscreen by the application. This means it can display the new position as close to instantly as possible, whereas a normal 3D rendering pipeline might take quite some time for a rendered frame to work its way through to the display.

Advertisement

How are you invoking your actual rendering? Is it in the WM_PAINT handler of your message loop, by chance?

(If not, please show the code that invokes it and the code that drives you main loop... and probably your actual window procedure as well; those are the likely culprits).

The drawing code is in the game loop and it is called when there are no messages from PeekMessage, inbetween the BeginDraw() and EndDraw() calls, and I'm running it in the Debug mode but without the debug (CTRL+F5 on visual studio).

Framework2D "D2DApp" code:

Spoiler


#include "D2DApp.h"
#include <iostream>
#include <sstream>

LRESULT CALLBACK WindowProc(HWND hwnd, UINT msg, WPARAM wParam, LPARAM lParam)
{
	D2DApp* pD2DApp = reinterpret_cast<D2DApp*>(GetWindowLongPtr(hwnd, GWLP_USERDATA));
	if (pD2DApp) { return pD2DApp->WinProc(hwnd, msg, wParam, lParam); }
	else { return DefWindowProc(hwnd, msg, wParam, lParam); }
}


D2DApp::D2DApp(HINSTANCE instance, std::wstring caption, float width, float height)
	: mInstance{ instance }, mhWindow{ nullptr }, mClientWidth{ width }, mClientHeight{ height }, mWindowCaption{ caption },
	mTimer{}, mAppPaused{ false }, mFactory{ nullptr }, mRenderTarger{ nullptr }, mBrush{ nullptr }
{
}


D2DApp::~D2DApp()
{
	if (mBrush) { mBrush->Release(); }
	if (mRenderTarger) { mRenderTarger->Release(); }
	if (mFactory) { mFactory->Release(); }
}

bool D2DApp::Init()
{
	//Register Window
	WNDCLASS mainWindow{};
	mainWindow.lpfnWndProc = WindowProc;
	mainWindow.hInstance = mInstance;
	mainWindow.hCursor = LoadCursor(mInstance, IDC_ARROW);
	mainWindow.lpszClassName = L"mainWindow";
	if (!RegisterClassW(&mainWindow))
	{
		MessageBoxW(mhWindow, L"Couldn't register the window", 0, 0);
		return false;
	}
	//Create Window
	RECT rect{ 0,0,mClientWidth,mClientHeight };
	AdjustWindowRect(&rect, WS_OVERLAPPEDWINDOW, false);
	int newWidth = rect.right - rect.left;
	int newHeight = rect.bottom - rect.top;
	int monitorWidth = GetSystemMetrics(SM_CXSCREEN);
	int monitorHeight = GetSystemMetrics(SM_CYSCREEN);
	mhWindow = CreateWindowExW(0, mainWindow.lpszClassName, mWindowCaption.c_str(), WS_OVERLAPPEDWINDOW | WS_VISIBLE,
							   monitorWidth / 2 - newWidth / 2, monitorHeight / 2 - newHeight / 2, newWidth, newHeight,
							   0, 0, mInstance, 0);
	if (!mhWindow)
	{
		MessageBoxW(mhWindow, L"Couldn't create the window", 0, 0);
		return false;
	}
	UpdateWindow(mhWindow);
	SetWindowLongPtr(mhWindow, GWLP_USERDATA, reinterpret_cast<LONG_PTR>(this));

	//Init D2D
	HRESULT hr;

	//Create Factory
	hr = D2D1CreateFactory(D2D1_FACTORY_TYPE_SINGLE_THREADED, &mFactory);
	if (hr != S_OK) {
		MessageBoxW(mhWindow, L"Couldn't create the D2D1 factory", 0, 0);
		return false;
	}

	//Create HwndRenderTarget
	D2D1_RENDER_TARGET_PROPERTIES RTProp{};
	RTProp.type = D2D1_RENDER_TARGET_TYPE_HARDWARE;
	RTProp.pixelFormat.format = DXGI_FORMAT_R8G8B8A8_UNORM;
	D2D1_HWND_RENDER_TARGET_PROPERTIES hWndRTProp{};
	hWndRTProp.hwnd = mhWindow;
	hWndRTProp.pixelSize.width = mClientWidth;
	hWndRTProp.pixelSize.height = mClientHeight;

	hr = mFactory->CreateHwndRenderTarget(RTProp, hWndRTProp, &mRenderTarger);
	if (hr != S_OK) {
		MessageBoxW(mhWindow, L"Couldn't create the HwndRenderTarget", 0, 0);
		return false;
	}

	//Create Solid Brush
	hr = mRenderTarger->CreateSolidColorBrush(D2D1::ColorF(0.4f, 0.3f, 0.7f, 1.0f), &mBrush);
	if (hr != S_OK) {
		MessageBoxW(mhWindow, L"Couldn't create the solid brush", 0, 0);
		return false;
	}

	return true;
}
int D2DApp::Run()
{
	MSG msg{};
	mTimer.reset();
	while (msg.message != WM_QUIT)
	{
		if (PeekMessageW(&msg, 0, 0, 0, PM_REMOVE))
		{
			TranslateMessage(&msg);
			DispatchMessageW(&msg);
		}
		else
		{
			//Timer Tick
			mTimer.tick();

			if (!mAppPaused)
			{
				CalculateFrameStats();
				Update(mTimer.deltaTime());

				BeginDraw();
				Draw();
				EndDraw();
			}
			else
			{
				Sleep(100);
			}

		}
	}

	return (int)msg.wParam;
}
void D2DApp::Update(float dt)
{
}
void D2DApp::Draw()
{
}
void D2DApp::BeginDraw()
{
	mRenderTarger->BeginDraw();
}

void D2DApp::EndDraw()
{
	mRenderTarger->EndDraw();
}

LRESULT D2DApp::WinProc(HWND hwnd, UINT msg, WPARAM wparam, LPARAM lparam)
{
	switch (msg)
	{
	case WM_DESTROY:
	case WM_CLOSE:
	{
		PostQuitMessage(0);
	}break;
	}
	return DefWindowProc(hwnd, msg, wparam, lparam);
}

void D2DApp::CalculateFrameStats()
{
	static int frameCnt = 0;
	static float timeElapsed = 0.0f;
	frameCnt++;

	if ((mTimer.totalTime() - timeElapsed) >= 1.0f)
	{
		float fps = (float)frameCnt; 
		float mspf = 1000.0f / fps;

		std::wostringstream outs;
		//outs.precision(6);
		outs << mWindowCaption.c_str() << "    "
			<< "FPS: " << fps << "    "
			<< "Frame Time: " << mspf << " (ms)";
		SetWindowTextW(mhWindow, outs.str().c_str());

		frameCnt = 0;
		timeElapsed += 1.0f;
	}
}

 

 

class that inherits from the Framework2D "D2DApp" and run everything (does the drawing calls) the:

Spoiler


#include "TrigClient.h"
#include <DirectXMath.h>
#include <iostream>
using std::cout; using std::endl;

#define rad(x) DirectX::XMConvertToRadians(x)
#define deg(x) DirectX::XMConvertToDegrees(x)

TrigClient::TrigClient(HINSTANCE instance, std::wstring caption, float width, float height)
    :D2DApp(instance, caption, width, height), mOriginX{ 0 }, mOriginY{ 0 }, mAngle{ 0 }, mAngleChange{ 70.f },
    mWaveBox{}, mMouse{ 0 }, mDashedLine{ nullptr }, mTriangleGeo{ nullptr }
{
}


TrigClient::~TrigClient()
{
    if (mDashedLine) { mDashedLine->Release(); }
    if (mTriangleGeo) { mTriangleGeo->Release(); }
}

bool TrigClient::Init()
{
    if (!D2DApp::Init())
    {
        return false;
    }
    //Additional initialization
    mOriginX = { mClientWidth / 2.f };
    mOriginY = { mClientHeight / 3.5f };

    //waveBox
    float waveBoxPadding = 50.f;
    float  waveBoxWidth = 350.f;
    float  waveBoxHeight = 250.f;
    mWaveBox.bottom = mClientHeight - waveBoxPadding;
    mWaveBox.top = mWaveBox.bottom - waveBoxHeight;
    mWaveBox.left = waveBoxPadding;
    mWaveBox.right = mWaveBox.left + waveBoxWidth;

    D2D1_STROKE_STYLE_PROPERTIES dashedLineProp{};
    dashedLineProp.startCap = D2D1_CAP_STYLE_FLAT;
    dashedLineProp.endCap = D2D1_CAP_STYLE_FLAT;
    dashedLineProp.dashCap = D2D1_CAP_STYLE_ROUND;
    dashedLineProp.lineJoin = D2D1_LINE_JOIN_MITER;
    dashedLineProp.miterLimit = 60.f;
    dashedLineProp.dashStyle = D2D1_DASH_STYLE_CUSTOM; //try DOT
    dashedLineProp.dashOffset = 3.3f; // might want to use this to animate the dashLine
    float dashes[] = { 6.0f, 6.0f, 6.0f };

    HRESULT hr = mFactory->CreateStrokeStyle(dashedLineProp, dashes, ARRAYSIZE(dashes), &mDashedLine);
    if (hr != S_OK) {
        MessageBoxW(mhWindow, L"failed to create dashed line", 0, 0);
        return false;
    }

    return true;
}

void TrigClient::Update(float dt)
{
    mAngle += mAngleChange*dt;

    float oneCircleTime = 360 / mAngleChange; //time required to complete 1 revolution, in seconds
    unsigned pointResolution = 30;
    float pointsPerSecond = 1 / pointResolution; //how often points are added, in seconds

    static float addPointTimer = 0;
    addPointTimer += dt;
    //evaluate every t | determines wave resolution
    if (addPointTimer >= pointsPerSecond)
    {
        //function to turn into a wave
        D2D1_POINT_2F newPoint = { mTimer.totalTime() ,cos(rad(mAngle)) };
        
        //add new point
        mWavePoints.push_back(newPoint);

        //remove old Points
        for (size_t i = 0; i < mWavePoints.size(); i++)
        {
            if (mTimer.totalTime() - mWavePoints.front().x >= oneCircleTime) {
                mWavePoints.pop_front();
            }
            else {
                break;
            }
        }

        addPointTimer = 0;
    }
}

void TrigClient::Draw()
{
    //background
    mRenderTarger->Clear(D2D1::ColorF(0.41f, 0.41f, 0.41f, 1.0f));
    makeGrid(25);

    //circle stuff
    float radius = 250.f;
    D2D1_POINT_2F point1 = D2D1::Point2F(radius * cos(rad(mAngle)), radius * sin(rad(mAngle)));
    circileFromOrigin(radius, 3.f, D2D1::ColorF(0.2f, 0.65f, 0.86f, 1.f));
    lineFromOrigin(point1, 2.f, D2D1::ColorF(0.46f, 0.76f, 0.86f, 1.f));
    drawPointFromOrigin(point1, 6.f, D2D1::ColorF(1.0f, 0.15f, 0.30f, 1.f));
    projectionX(point1);
    perpX(point1);
    triangleAreaDisplay(point1);

    //draw wave box
    drawWaveBox(cos(rad(mAngle)));
}

void TrigClient::drawWaveBox(const float point)
{
    //transparent box base
    mBrush->SetColor(D2D1::ColorF(0.4f, 0.4f, 0.4f, 0.65f));
    mRenderTarger->FillRectangle(mWaveBox, mBrush);
    //box outline
    mBrush->SetColor(D2D1::ColorF(0.9f, 0.9f, 0.0f, 0.45f));
    mRenderTarger->DrawRectangle(mWaveBox, mBrush);

    //horizontal line
    mBrush->SetColor(D2D1::ColorF(0.08f, 0.08f, 0.08f, 1.0f));
    float boxWidth = mWaveBox.right - mWaveBox.left;
    float boxHeight = mWaveBox.bottom - mWaveBox.top;
    float marks = 50;
    float linePaddingH = 20;
    float linePaddingV = 10;
    float lineBoundLeft = mWaveBox.left + linePaddingH;
    float lineBoundRight = mWaveBox.right - linePaddingH;
    float lineBoundBottom = mWaveBox.bottom - linePaddingV;
    float lineLength = lineBoundRight - lineBoundLeft;
    float intervalSpace = lineLength / marks;

    mRenderTarger->DrawLine(D2D1::Point2(lineBoundLeft, lineBoundBottom),
                            D2D1::Point2(lineBoundRight, lineBoundBottom), mBrush, 1.f);
    //horizontal marks
    for (size_t i = 0; i <= marks; i++) {
        mRenderTarger->DrawLine(D2D1::Point2(lineBoundLeft + (intervalSpace*i), lineBoundBottom - 5),
                                D2D1::Point2(lineBoundLeft + (intervalSpace*i), lineBoundBottom + 5), mBrush, 1.f);
    }

    //internal box
    float internalBoxTop = mWaveBox.top + (mWaveBox.bottom - lineBoundBottom) + linePaddingV;
    float internalBoxBottom = lineBoundBottom - linePaddingV;
    mBrush->SetColor(D2D1::ColorF(0.24f, 0.24f, 0.24f, 0.85f));
    mRenderTarger->FillRectangle(D2D1::RectF(lineBoundLeft, //left
                                             internalBoxTop, //top
                                             lineBoundRight, //right
                                             internalBoxBottom),// bottom
                                 mBrush);
    //draw wave
    float internalBoxMidY = internalBoxTop + ((internalBoxBottom - internalBoxTop) / 2);
    float oneCircleTime = 360 / mAngleChange;
    float graphHSpace = lineBoundRight - lineBoundLeft;
    float graphYSpace = internalBoxBottom - internalBoxTop;
    for (auto& point : mWavePoints)
    {
        float pointAge = mTimer.totalTime() - point.x;
        drawPoint(D2D1::Point2F(lineBoundLeft + ((graphHSpace / oneCircleTime) * pointAge),//X
                                internalBoxMidY + (-point.y * graphYSpace / 2.05f))//Y
                  , 2.6f, D2D1::ColorF(-point.y, point.y, point.y, 0.85f));
    }
}

void TrigClient::projectionX(const D2D1_POINT_2F& p)
{
    mBrush->SetColor(D2D1::ColorF(0.5f, 0.75f, 0.30f, 1.f));
    mRenderTarger->DrawLine(D2D1::Point2F(mOriginX + p.x, mOriginY - p.y), D2D1::Point2F(mOriginX + p.x, mOriginY), mBrush, 1.f, mDashedLine);
    drawPointFromOrigin(D2D1::Point2F(p.x, 0), 5.f, D2D1::ColorF(0.5f, 0.75f, 0.30f, 1.f));
}

void TrigClient::perpX(const D2D1_POINT_2F& p)
{
    mBrush->SetColor(D2D1::ColorF(0.5f, 0.75f, 0.30f, 1.f));
    mRenderTarger->DrawLine(D2D1::Point2F(mOriginX + p.x, mOriginY - p.y), D2D1::Point2F(mOriginX, mOriginY - p.y), mBrush, 1.f, mDashedLine);
    drawPointFromOrigin(D2D1::Point2F(0, p.y), 5.f, D2D1::ColorF(0.5f, 0.75f, 0.30f, 1.f));
}

void TrigClient::triangleAreaDisplay(const D2D1_POINT_2F& p)
{
    //free previous triangle
    if (mTriangleGeo) { mTriangleGeo->Release(); }

    //create triangle path geo
    HRESULT hr = mFactory->CreatePathGeometry(&mTriangleGeo);
    if (hr != S_OK) {
        assert(hr == S_OK);
        MessageBoxW(mhWindow, L"failed to create triangle path geo", 0, 0);
    }

    ID2D1GeometrySink* pSink;
    D2D1_POINT_2F tringle[3] = {
                                { mOriginX + p.x,mOriginY},
                                { mOriginX + p.x , mOriginY - p.y},
                                { mOriginX, mOriginY }
    };

    mTriangleGeo->Open(&pSink);
    pSink->BeginFigure(D2D1::Point2F(mOriginX, mOriginY), D2D1_FIGURE_BEGIN_FILLED);
    //build shape here//
    pSink->AddLines(tringle, 3);
    ////////////
    pSink->EndFigure(D2D1_FIGURE_END_CLOSED);
    pSink->Close();
    pSink->Release();

    //draw shape
    float point = cos(rad(mAngle));
    mBrush->SetColor(D2D1::ColorF(-point, point, point, 0.7f));
    mRenderTarger->FillGeometry(mTriangleGeo, mBrush);
}

void TrigClient::makeGrid(float spacing)
{
    mBrush->SetColor(D2D1::ColorF(0.0f, 0.0f, 0.0f, 1.0f));
    //Major Horizontal and Vertical lines
    mRenderTarger->DrawLine(D2D1::Point2F(0.0f, mOriginY),
                            D2D1::Point2F(mClientWidth, mOriginY),
                            mBrush, 2.f);
    mRenderTarger->DrawLine(D2D1::Point2F(mOriginX, 0.0f),
                            D2D1::Point2F(mOriginX, mClientHeight),
                            mBrush, 2.f);

    //Vertical lines

    unsigned leftLines = (int)(mOriginX / spacing);
    unsigned righttLines = (int)((mClientWidth - mOriginX) / spacing);
    unsigned verticalLines = leftLines > righttLines ? leftLines : righttLines;

    for (size_t i = 1; i <= verticalLines; i++)
    {
        //left
        mRenderTarger->DrawLine(D2D1::Point2F(mOriginX - (i*spacing), 0.f),
                                D2D1::Point2F(mOriginX - (i*spacing), mClientHeight),
                                mBrush, 0.25f);
        //right
        mRenderTarger->DrawLine(D2D1::Point2F(mOriginX + (i*spacing), 0.f),
                                D2D1::Point2F(mOriginX + (i*spacing), mClientHeight),
                                mBrush, 0.25f);
    }

    //Horizontal lines

    unsigned topLines = (int)(mOriginY / spacing);
    unsigned bottomtLines = (int)((mClientHeight - mOriginY) / spacing);
    unsigned horizontalLines = topLines > bottomtLines ? topLines : bottomtLines;
    for (size_t i = 1; i <= horizontalLines; i++)
    {
        //up
        mRenderTarger->DrawLine(D2D1::Point2F(0.f, mOriginY - (i*spacing)),
                                D2D1::Point2F(mClientWidth, mOriginY - (i*spacing)),
                                mBrush, 0.25f);
        //down
        mRenderTarger->DrawLine(D2D1::Point2F(0.f, mOriginY + (i*spacing)),
                                D2D1::Point2F(mClientWidth, mOriginY + (i*spacing)),
                                mBrush, 0.25f);
    }
}

void TrigClient::lineFromOrigin(const D2D1_POINT_2F& p, float strokeSize, const D2D1_COLOR_F& color)
{
    if (color.a != -1.f) {
        mBrush->SetColor(color);
    }
    mRenderTarger->DrawLine(D2D1::Point2F(mOriginX, mOriginY),
                            D2D1::Point2F(mOriginX + p.x, mOriginY - p.y), mBrush, strokeSize);
}

void TrigClient::circileFromOrigin(const float radius, float strokeSize, const D2D1_COLOR_F& color)
{
    if (color.a != -1.f) {
        mBrush->SetColor(color);
    }
    mRenderTarger->DrawEllipse(D2D1::Ellipse(D2D1::Point2F(mOriginX, mOriginY), radius, radius),
                               mBrush, strokeSize);
}

void TrigClient::drawPoint(const D2D1_POINT_2F& p, float radius, const D2D1_COLOR_F& color)
{
    if (color.a != -1.f) {
        mBrush->SetColor(color);
    }
    mRenderTarger->FillEllipse(D2D1::Ellipse(D2D1::Point2F(p.x, p.y), radius, radius), mBrush);
}

void TrigClient::drawPointFromOrigin(const D2D1_POINT_2F& p, float radius, const D2D1_COLOR_F& color)
{
    if (color.a != -1.f) {
        mBrush->SetColor(color);
    }
    mRenderTarger->FillEllipse(D2D1::Ellipse(D2D1::Point2F(mOriginX + p.x, mOriginY - p.y), radius, radius),
                               mBrush);
}

 

Since is a long read of code and I don't really need to have a circle that follow my mouse in real time don't sweat it trying to solve this guys, I don't even have the code for that circle in there anymore.

But if anyone has more insight into this I would still like to know more :)

Also is worth mentioning I have fixed 60FPS by default and I believe it has to do with the D2D intialization, so maybe is just D2D 

The most obvious factor is that the Sleep(100) in your core loop will always halt your application (and thus your updating and rendering of the position of the shape) for at least 100 milliseconds, if not more. The cursor, being driven by the OS, blissfully does not care about that and will update the cursor in realtime.

That Sleep() is not needed. It's often seen used to "make sure we don't use 100% of the CPU," but that goal itself is often based on a false premise and even then, sleeping the whole thread for an arbitrary amount of time isn't the solution.

If your base class WndProc is doing anything with mouse-moved events that is time-consuming, that can also throttle back your core loop since mouse-moved events are _very_ spammy. Similarly, if the base framework class is trying to force a WM_PAINT, which is a low-priority but spammy (even though it's coelsced) message, and doesn't actually correctly handle that WM_PAINT, that can also throttle back your core loop by causing the branch into the positive case for the PeekMessage check to be taken way, way more frequently than you generally want. I'd verify those.

That sleep is actually never called because the if (!mAppPaused) is always true since I am not handling pause, I am not processing any inputs and I have no WM_PAINT cases :P

Wish I knew how to share the exe with you guys so you can feel the lag for yourself :/

Advertisement

So the "paused" clause is literally for like, "user has paused the game" or otherwise suspended the application and not to pause the loop to throttle down to your target framerate? That does mean the sleep is probably not the cause of your lag, unless you're accidentally flip-flopping the state of the gating variable someplace (breakpoint in the debugger to verify), although the Sleep() is still not the best way to handle that. Neither here nor there, though.

You can upload the .exe someplace, although the chance that somebody will download a random .exe is slim (and you'll want to make sure you upload an appropriately self-contained package, e.g., not a debug build etc). You could also upload a .zip of the project and somebody might be willing to build and verify it for you. Or try making a video and sharing that.

Something else you might try as a diagnostics technique is to make the shape move back and forth on its own, without mouse input. Then observe the behavior as it runs, keeping your mouse still and your hands off the keyboard. Then, observe the behavior as it runs while you move the mouse frantically over the window. This can help rule out whether the delay is due to a factor of the Win32 message queue size or simply a constant issue.

There is a 4th possibility, searching this awesome forum for a solution, which is 7 years old :D  

mind in a box said:

Quote

This is because VSync. To turn it off, just do that when you create the rendertarget:

V_RETURN(MyVars.D2DFactory->CreateHwndRenderTarget( RenderTargetProperties(D2D1_RENDER_TARGET_TYPE_DEFAULT,PixelFormat(DXGI_FORMAT_UNKNOWN ,D2D1_ALPHA_MODE_PREMULTIPLIED )), HwndRenderTargetProperties( hw, SizeU( WindowSize.right - WindowSize.left, WindowSize.bottom - WindowSize.top),D2D1_PRESENT_OPTIONS_IMMEDIATELY ), &MyVars.RT));

This will unlock your FPS.

EDIT: Oh, didn't read you post carefully. :( But try setting off vsync, and please show how you set up Direct2D. In my engine, it renders just very fast...

And indeed, switching to D2D1_PRESENT_OPTIONS_IMMEDIATELY that 5cm of lag turned into 2-3mm, much much better! :D

Still thanks for all the good tips jpetrie, and double thanks to mind in a box for the (almost) perfect solution :)  (perfect would be 0mm :P )

It's almost like I mentioned rendering latency in my first reply! (Almost, because I probably wasn't explicit enough about it.)

Be aware that D2D1_PRESENT_OPTIONS_IMMEDIATELY has side-effects. Make sure you can live with them.

2 minutes ago, Kylotan said:

Be aware that D2D1_PRESENT_OPTIONS_IMMEDIATELY has side-effects. Make sure you can live with them.

What are this side effects? GPU meltdown? :D

This topic is closed to new replies.

Advertisement