성능 최적화(Direct3D 9)

3D 그래픽을 사용하는 실시간 애플리케이션을 만드는 모든 개발자는 성능 최적화를 염려합니다. 이 섹션에서는 코드에서 최상의 성능을 얻기 위한 지침을 제공합니다.

일반 성능 팁

  • 필요한 경우에만 지우기.
  • 상태 변경을 최소화하고 나머지 상태 변경 내용을 그룹화합니다.
  • 이렇게 할 수 있는 경우 더 작은 텍스처를 사용합니다.
  • 장면에서 개체를 앞에서 뒤로 그립니다.
  • 목록 및 팬 대신 삼각형 스트립을 사용합니다. 최적의 꼭짓점 캐시 성능을 위해 삼각형 꼭짓점을 나중에 사용하지 않고 더 빨리 다시 사용하도록 스트립을 정렬합니다.
  • 시스템 리소스의 불균형한 공유가 필요한 특수 효과를 정상적으로 저하합니다.
  • 애플리케이션의 성능을 지속적으로 테스트합니다.
  • 꼭짓점 버퍼 스위치를 최소화합니다.
  • 가능한 경우 정적 꼭짓점 버퍼를 사용합니다.
  • 개체당 하나가 아닌 정적 개체에 대해 FVF당 하나의 큰 정적 꼭짓점 버퍼를 사용합니다.
  • 애플리케이션이 AGP 메모리의 꼭짓점 버퍼에 임의로 액세스해야 하는 경우 32바이트의 배수인 꼭짓점 형식 크기를 선택합니다. 그렇지 않으면 가장 작은 적절한 형식을 선택합니다.
  • 인덱싱된 기본 형식을 사용하여 그립니다. 이렇게 하면 하드웨어 내에서 더 효율적인 꼭짓점 캐싱을 수행할 수 있습니다.
  • 깊이 버퍼 형식에 스텐실 채널이 포함된 경우 항상 깊이 및 스텐실 채널을 동시에 지웁니다.
  • 가능한 경우 셰이더 명령과 데이터 출력을 결합합니다. 예:
    // Rather than doing a multiply and add, and then output the data with 
    //   two instructions:
    mad r2, r1, v0, c0
    mov oD0, r2
    
    // Combine both in a single instruction, because this eliminates an  
    //   additional register copy.
    mad oD0, r1, v0, c0 
    

데이터베이스 및 컬링

Direct3D에서 뛰어난 성능을 발휘하려면 세계에서 개체의 신뢰할 수 있는 데이터베이스를 빌드하는 것이 중요합니다. 래스터화 또는 하드웨어를 개선하는 것보다 더 중요합니다.

관리할 수 있는 가장 낮은 다각형 수를 유지해야 합니다. 처음부터 낮은 다각형 모델을 빌드하여 낮은 다각형 수를 설계합니다. 개발 프로세스의 뒷부분에서 성능을 희생하지 않고 다각형을 추가합니다. 가장 빠른 다각형은 그리지 않는 다각형입니다.

기본 형식 일괄 처리

실행 중에 최상의 렌더링 성능을 얻으려면 기본 형식을 일괄 처리로 사용하고 렌더링 상태 변경 횟수를 가능한 한 낮게 유지합니다. 예를 들어 텍스처가 두 개 있는 개체가 있는 경우 첫 번째 텍스처를 사용하는 삼각형을 그룹화하고 필요한 렌더링 상태로 따라 텍스처를 변경합니다. 그런 다음 두 번째 텍스처를 사용하는 모든 삼각형을 그룹화합니다. Direct3D에 대한 가장 간단한 하드웨어 지원은 HAL(하드웨어 추상화 계층)을 통해 렌더링 상태 및 기본 형식의 일괄 처리를 사용하여 호출됩니다. 명령이 일괄 처리될수록 실행 중에 수행되는 HAL 호출이 줄어듭니다.

조명 팁

조명은 렌더링된 각 프레임에 꼭짓점당 비용을 추가하므로 애플리케이션에서 사용하는 방법에 주의하여 성능을 크게 향상시킬 수 있습니다. 다음 팁의 대부분은 최대에서 파생됩니다. "가장 빠른 코드는 호출되지 않는 코드입니다."

  • 가능한 한 적은 수의 광원을 사용합니다. 예를 들어 전체 조명 수준을 높이려면 새 광원을 추가하는 대신 주변 광원을 사용합니다.
  • 방향 표시등은 점등 또는 스포트라이트보다 더 효율적입니다. 방향 광원의 경우 조명 방향이 고정되며 꼭짓점별로 계산할 필요가 없습니다.
  • 조명의 원뿔 외부 영역이 빠르게 계산되기 때문에 스포트라이트는 점등보다 더 효율적일 수 있습니다. 스포트라이트가 더 효율적인지 여부는 스포트라이트에 의해 조명되는 장면의 양에 따라 달라집니다.
  • 범위 매개 변수를 사용하여 조명을 조명해야 하는 장면의 부분으로만 제한합니다. 모든 광원 유형은 범위를 벗어나면 상당히 일찍 종료됩니다.
  • 반사 하이라이트는 조명 비용의 거의 두 배입니다. 반드시 사용해야 하는 경우에만 사용합니다. 가능하면 D3DRS_SPECULARENABLE 렌더링 상태를 기본값인 0으로 설정합니다. 재질을 정의할 때 해당 재질에 대한 반사 하이라이트를 해제하려면 반사 전원 값을 0으로 설정해야 합니다. 반사 색을 0,0,0으로 설정하는 것만으로는 충분하지 않습니다.

텍스처 크기

텍스처 매핑 성능은 메모리 속도에 크게 좌우됩니다. 애플리케이션 텍스처의 캐시 성능을 최대화하는 방법에는 여러 가지가 있습니다.

  • 텍스처를 작게 유지합니다. 텍스처가 작을수록 기본 CPU의 보조 캐시에서 유지 관리될 가능성이 높아집니다.
  • 기본 단위로 텍스처를 변경하지 마세요. 다각형을 사용하는 텍스처 순서대로 그룹화해 보세요.
  • 가능하면 제곱 텍스처를 사용합니다. 크기가 256x256인 텍스처가 가장 빠릅니다. 예를 들어 애플리케이션에서 4개의 128x128 텍스처를 사용하는 경우 동일한 색상표를 사용하고 모두 하나의 256x256 텍스처에 배치해 보세요. 또한 이 기술은 텍스처 교환의 양을 줄입니다. 물론 애플리케이션에서 설명한 대로 텍스처를 가능한 한 작게 유지해야 하므로 텍스처를 많이 필요로 하지 않는 한 256x256 텍스처를 사용하면 안 됩니다.

매트릭스 변환

Direct3D는 여러 내부 데이터 구조를 구성하기 위해 설정한 월드 매트릭스와 보기 매트릭스를 사용합니다. 새로운 월드 매트릭스 또는 보기 매트릭스를 설정할 때마다 시스템에서 관련 내부 구조를 다시 계산합니다. 이러한 행렬을 자주 설정(예: 프레임당 수천 번)은 계산에 시간이 많이 걸립니다. 월드 매트릭스 및 보기 매트릭스를 월드 매트릭스로 설정한 월드 보기 매트릭스에 연결한 다음, 보기 매트릭스를 ID로 설정하면 필요한 계산의 수를 최소화할 수 있습니다. 필요에 따라 월드 매트릭스를 수정하고 연결하고 초기화할 수 있도록 개별 월드 및 보기 매트릭스의 캐시된 복사본을 보관해 두세요. 이 설명서의 명확성을 위해 Direct3D 샘플은 이 최적화를 거의 채택하지 않습니다.

동적 텍스처 사용

드라이버가 동적 텍스처를 지원하는지 확인하려면 D3DCAPS9 구조체의 D3DCAPS2_DYNAMICTEXTURES 플래그를 검사.

동적 텍스처로 작업할 때 다음 사항에 유의하세요.

  • 관리할 수 없습니다. 예를 들어 풀을 D3DPOOL_MANAGED 수 없습니다.
  • 동적 텍스처는 D3DPOOL_DEFAULT 만들어지더라도 잠글 수 있습니다.
  • D3DLOCK_DISCARD 동적 텍스처에 유효한 잠금 플래그입니다.

형식당 하나만 동적 텍스처를 만들고 크기당 가능하도록 만드는 것이 좋습니다. 동적 Mipmap, 큐브 및 볼륨은 모든 수준을 잠그는 추가 오버헤드 때문에 권장되지 않습니다. mipmap의 경우 D3DLOCK_DISCARD 최상위 수준에서만 허용됩니다. 모든 수준은 최상위 수준만 잠그면 삭제됩니다. 이 동작은 볼륨 및 큐브에 대해 동일합니다. 큐브의 경우 최상위 수준 및 얼굴 0이 잠깁니다.

다음 의사 코드는 동적 텍스처를 사용하는 예제를 보여 줍니다.

DrawProceduralTexture(pTex)
{
    // pTex should not be very small because overhead of 
    //   calling driver every D3DLOCK_DISCARD will not 
    //   justify the performance gain. Experimentation is encouraged.
    pTex->Lock(D3DLOCK_DISCARD);
    <Overwrite *entire* texture>
    pTex->Unlock();
    pDev->SetTexture();
    pDev->DrawPrimitive();
}

동적 꼭짓점 및 인덱스 버퍼 사용

그래픽 프로세서가 버퍼를 사용하는 동안 정적 꼭짓점 버퍼를 잠그면 상당한 성능 저하가 발생할 수 있습니다. 잠금 호출은 그래픽 프로세서가 버퍼에서 꼭짓점 또는 인덱스 데이터 읽기가 완료될 때까지 기다려야 호출 애플리케이션으로 돌아갈 수 있으며 상당한 지연이 발생할 수 있습니다. 프레임당 여러 번 고정 버퍼에서 잠금 및 렌더링하면 그래픽 프로세서가 렌더링 명령을 버퍼링하지 못하게 됩니다. 잠금 포인터를 반환하기 전에 명령을 완료해야 하기 때문에 버퍼링된 명령이 없으면 애플리케이션이 꼭짓점 버퍼 또는 인덱스 버퍼를 채우고 렌더링 명령을 실행할 때까지 그래픽 프로세서가 유휴 상태로 유지됩니다.

꼭짓점 또는 인덱스 데이터는 변경되지 않는 것이 가장 좋지만 항상 가능한 것은 아닙니다. 애플리케이션에서 꼭짓점을 변경하거나 프레임당 여러 번 데이터를 인덱싱해야 하는 경우가 많습니다. 이러한 경우 꼭짓점 또는 인덱스 버퍼는 D3DUSAGE_DYNAMIC 사용하여 만들어야 합니다. 이 사용 플래그로 인해 Direct3D가 자주 잠금 작업을 최적화합니다. D3DUSAGE_DYNAMIC 버퍼가 자주 잠겨 있는 경우에만 유용합니다. 상수로 남아 있는 데이터는 정적 꼭짓점 또는 인덱스 버퍼에 배치해야 합니다.

동적 꼭짓점 버퍼를 사용할 때 성능 향상을 받으려면 애플리케이션에서 적절한 플래그를 사용하여 IDirect3DVertexBuffer9::Lock 또는 IDirect3DIndexBuffer9::Lock 을 호출해야 합니다. D3DLOCK_DISCARD 애플리케이션이 버퍼에 이전 꼭짓점 또는 인덱스 데이터를 유지할 필요가 없음을 나타냅니다. D3DLOCK_DISCARD 잠금을 호출할 때 그래픽 프로세서가 버퍼를 계속 사용하는 경우 이전 버퍼 데이터 대신 새 메모리 영역에 대한 포인터가 반환됩니다. 이렇게 하면 애플리케이션이 새 버퍼에 데이터를 배치하는 동안 그래픽 프로세서에서 이전 데이터를 계속 사용할 수 있습니다. 애플리케이션에 추가 메모리 관리가 필요하지 않습니다. 그래픽 프로세서가 완료되면 이전 버퍼가 자동으로 재사용되거나 제거됩니다. D3DLOCK_DISCARD 버퍼를 잠그면 항상 전체 버퍼가 삭제되고 0이 아닌 오프셋 또는 제한된 크기 필드를 지정해도 버퍼의 잠금 해제된 영역에 정보가 유지되지 않습니다.

스프라이트를 렌더링하기 위해 4개의 꼭짓점을 추가하는 등 애플리케이션이 잠금당 저장해야 하는 데이터의 양이 작은 경우가 있습니다. D3DLOCK_NOOVERWRITE 애플리케이션이 동적 버퍼에서 이미 사용 중인 데이터를 덮어쓰지 않음을 나타냅니다. 잠금 호출은 이전 데이터에 대한 포인터를 반환하므로 애플리케이션이 꼭짓점 또는 인덱스 버퍼의 사용되지 않는 지역에 새 데이터를 추가할 수 있습니다. 그리기 작업에 사용되는 꼭짓점 또는 인덱스는 그래픽 프로세서에서 계속 사용 중일 수 있으므로 애플리케이션에서 수정해서는 안 됩니다. 그런 다음, 애플리케이션은 동적 버퍼가 가득 찬 후 D3DLOCK_DISCARD 사용하여 새 메모리 영역을 수신하고 그래픽 프로세서가 완료된 후 이전 꼭짓점 또는 인덱스 데이터를 삭제해야 합니다.

비동기 쿼리 메커니즘은 꼭짓점이 그래픽 프로세서에서 여전히 사용 중인지 확인하는 데 유용합니다. 꼭짓점을 사용하는 마지막 DrawPrimitive 호출 후 D3DQUERYTYPE_EVENT 형식의 쿼리를 실행합니다. IDirect3DQuery9::GetData가 S_OK 반환할 때 꼭짓점이 더 이상 사용되지 않습니다. D3DLOCK_DISCARD 또는 플래그가 없는 버퍼를 잠그면 꼭짓점이 그래픽 프로세서와 제대로 동기화되도록 보장되지만 플래그 없이 잠금을 사용하면 앞에서 설명한 성능 저하가 발생합니다. IDirect3DDevice9::BeginScene, IDirect3DDevice9::EndSceneIDirect3DDevice9::P resent와 같은 다른 API 호출은 그래픽 프로세서가 꼭짓점을 사용하여 완료되도록 보장하지 않습니다.

다음은 동적 버퍼와 적절한 잠금 플래그를 사용하는 방법입니다.

    // USAGE STYLE 1
    // Discard the entire vertex buffer and refill with thousands of vertices.
    // Might contain multiple objects and/or require multiple DrawPrimitive 
    //   calls separated by state changes, etc.
 
    // Determine the size of data to be moved into the vertex buffer.
    UINT nSizeOfData = nNumberOfVertices * m_nVertexStride;
 
    // Discard and refill the used portion of the vertex buffer.
    CONST DWORD dwLockFlags = D3DLOCK_DISCARD;
    
    // Lock the vertex buffer.
    BYTE* pBytes;
    if( FAILED( m_pVertexBuffer->Lock( 0, 0, &pBytes, dwLockFlags ) ) )
        return false;
    
    // Copy the vertices into the vertex buffer.
    memcpy( pBytes, pVertices, nSizeOfData );
    m_pVertexBuffer->Unlock();
 
    // Render the primitives.
    m_pDevice->DrawPrimitive( D3DPT_TRIANGLELIST, 0, nNumberOfVertices/3)
    // USAGE STYLE 2
    // Reusing one vertex buffer for multiple objects
 
    // Determine the size of data to be moved into the vertex buffer.
    UINT nSizeOfData = nNumberOfVertices * m_nVertexStride;
 
    // No overwrite will be used if the vertices can fit into 
    //   the space remaining in the vertex buffer.
    DWORD dwLockFlags = D3DLOCK_NOOVERWRITE;
    
    // Check to see if the entire vertex buffer has been used up yet.
    if( m_nNextVertexData > m_nSizeOfVB - nSizeOfData )
    {
        // No space remains. Start over from the beginning 
        //   of the vertex buffer.
        dwLockFlags = D3DLOCK_DISCARD;
        m_nNextVertexData = 0;
    }
    
    // Lock the vertex buffer.
    BYTE* pBytes;
    if( FAILED( m_pVertexBuffer->Lock( (UINT)m_nNextVertexData, nSizeOfData, 
               &pBytes, dwLockFlags ) ) )
        return false;
    
    // Copy the vertices into the vertex buffer.
    memcpy( pBytes, pVertices, nSizeOfData );
    m_pVertexBuffer->Unlock();
 
    // Render the primitives.
    m_pDevice->DrawPrimitive( D3DPT_TRIANGLELIST, 
               m_nNextVertexData/m_nVertexStride, nNumberOfVertices/3)
 
    // Advance to the next position in the vertex buffer.
    m_nNextVertexData += nSizeOfData;

메시 사용

인덱싱된 삼각형 스트립 대신 Direct3D 인덱싱된 삼각형을 사용하여 메시를 최적화할 수 있습니다. 하드웨어는 연속 삼각형의 95 %가 실제로 스트립을 형성하고 그에 따라 조정한다는 것을 발견 할 것입니다. 많은 드라이버가 이전 하드웨어에서도 이 작업을 수행합니다.

D3DX 메시 개체에는 해당 얼굴의 특성이라고 하는 DWORD로 태그가 지정된 각 삼각형 또는 얼굴이 있을 수 있습니다. DWORD의 의미 체계는 사용자 정의입니다. D3DX에서 메시를 하위 집합으로 분류하는 데 사용됩니다. 애플리케이션은 ID3DXMesh::LockAttributeBuffer 호출을 사용하여 얼굴별 특성을 설정합니다. ID3DXMesh::Optimize 메서드에는 D3DXMESHOPT_ATTRSORT 옵션을 사용하여 특성에서 메시 꼭짓점 및 얼굴을 그룹화할 수 있는 옵션이 있습니다. 이 작업이 완료되면 메시 개체는 ID3DXBaseMesh::GetAttributeTable을 호출하여 애플리케이션에서 가져올 수 있는 특성 테이블을 계산합니다. 메시가 특성별로 정렬되지 않은 경우 이 호출은 0을 반환합니다. 애플리케이션이 ID3DXMesh::Optimize 메서드에 의해 생성되므로 특성 테이블을 설정할 수 있는 방법은 없습니다. 특성 정렬은 데이터를 구분하므로 애플리케이션에서 메시가 특성 정렬임을 알고 있는 경우에도 ID3DXMesh::Optimize 를 호출하여 특성 테이블을 생성해야 합니다.

다음 topics 메시의 다양한 특성을 설명합니다.

특성 ID

특성 ID는 얼굴 그룹을 특성 그룹과 연결하는 값입니다. 이 ID는 ID3DXBaseMesh::D rawSubset이 그려야 하는 얼굴의 하위 집합을 설명합니다. 특성 ID는 특성 버퍼의 얼굴에 대해 지정됩니다. 특성 ID의 실제 값은 32비트에서 적합한 값일 수 있지만 n이 특성 수인 경우 0에서 n까지 사용하는 것이 일반적입니다.

특성 버퍼

특성 버퍼는 각 얼굴이 속한 특성 그룹을 지정하는 DWORD(얼굴당 하나씩)의 배열입니다. 이 버퍼는 메시를 만들 때 0으로 초기화되지만 부하 루틴으로 채워지거나 ID가 0인 둘 이상의 특성이 필요한 경우 사용자가 채워야 합니다. 이 버퍼에는 ID3DXMesh::Optimize의 특성에 따라 메시를 정렬하는 데 사용되는 정보가 포함되어 있습니다. 특성 테이블이 없으면 ID3DXBaseMesh::D rawSubset 는 이 버퍼를 검사하여 그릴 지정된 특성의 얼굴을 선택합니다.

특성 테이블

특성 테이블은 메시가 소유하고 유지 관리하는 구조체입니다. 생성되는 유일한 방법은 특성 정렬 또는 더 강력한 최적화를 사용하도록 설정된 ID3DXMesh::Optimize 를 호출하는 것입니다. 특성 테이블은 ID3DXBaseMesh::D rawSubset에 대한 단일 그리기 기본 호출을 빠르게 시작하는 데 사용됩니다. 다른 용도로는 진행 중인 메시도 이 구조를 유지하므로 현재 세부 수준에서 활성 상태인 얼굴과 꼭짓점을 확인할 수 있습니다.

Z 버퍼 성능

애플리케이션은 z-버퍼링과 질감을 사용할 때 장면을 앞에서 뒤로 렌더링하는 방식으로 성능을 높일 수 있습니다. 질감이 적용된 z-버퍼 화면은 스캔 라인 기반으로 z-버퍼를 대상으로 미리 테스트됩니다. 이전에 렌더링된 다각형에 의해 스캔 라인이 숨겨지면, 시스템은 신속하고 효율적으로 이를 거부합니다. Z-버퍼링의 성능을 향상시키는 방법도 있지만, 장면에서 두 번 이상 같은 픽셀을 그릴 때 매우 유용한 기술도 있습니다. 이러한 계산을 정확하게 수행하기 어렵지만, 유사한 값을 얻는 것은 어렵지 않습니다. 동일한 픽셀을 한 번만 그리는 경우, z-버퍼링을 끄고 화면을 뒤에서 앞으로 다시 그리는 것으로 최고의 성능을 달성할 수 있습니다.

프로그래밍 팁