Files
t5de-renderer/renderer/scene.cpp
T
dhkatzandClaude Opus 5.5 299cd8c312 Fix particle sizes, and add tools for comparing renderers
Particles: Direct3D 9's size limit evidently isn't the documented 64 pixels; unclamped (up to 2048) they match the client's renderer. A compare view shows the left half's scene over the right half, both renderers side by side; %TEMP%\t5de-renderer-view.txt switches views and %TEMP%\t5de-renderer-shaders\fixed_function.hlsl replaces the shaders while the client runs. The renderer process runs from a copy, so the program can be replaced meanwhile. The adapter recognizes only the textures and buffers it made (others reach the client renderer unchanged) and logs the others and render targets.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-28 06:43:34 -07:00

717 lines
29 KiB
C++

#include "scene.hpp"
#include <algorithm>
#include <array>
#include <cmath>
#include <cstring>
#include <string>
#include "fixed_function_hlsl.hpp"
#include "log.hpp"
namespace t5de {
using namespace Diligent;
using protocol::Op;
namespace {
void set_identity(float (&m)[16])
{
std::memset(m, 0, sizeof(m));
m[0] = m[5] = m[10] = m[15] = 1;
}
// An affine transform (3x3 row-major, then translation) as a 4x4 matrix for row vectors.
void set_transform(float (&m)[16], const float (&t)[12])
{
for (int row = 0; row < 3; ++row) {
m[row * 4 + 0] = t[row * 3 + 0];
m[row * 4 + 1] = t[row * 3 + 1];
m[row * 4 + 2] = t[row * 3 + 2];
m[row * 4 + 3] = 0;
}
m[12] = t[9];
m[13] = t[10];
m[14] = t[11];
m[15] = 1;
}
// u' = u * m[0] + v * m[1] + m[4], v' = u * m[2] + v * m[3] + m[5], as two rows dotted with (u, v, 1).
void set_texture_transform(float (&rows)[8], const float (&m)[6])
{
const float values[8] = {m[0], m[1], m[4], 0, m[2], m[3], m[5], 0};
std::memcpy(rows, values, sizeof(rows));
}
void set_color(float (&rgba)[4], std::uint32_t color)
{
rgba[0] = static_cast<float>((color >> 16) & 0xff) / 255.0f;
rgba[1] = static_cast<float>((color >> 8) & 0xff) / 255.0f;
rgba[2] = static_cast<float>(color & 0xff) / 255.0f;
rgba[3] = static_cast<float>(color >> 24) / 255.0f;
}
constexpr std::uint32_t hasNormals = 1;
constexpr std::uint32_t hasTexCoords = 2;
constexpr std::uint32_t hasColors = 4;
} // namespace
Scene::Scene(IRenderDevice* device, IDeviceContext* context, ISwapChain* swapChain)
: device_(device), context_(context), swapChain_(swapChain), shaderSource_(fixedFunctionHlsl)
{
BufferDesc constants;
constants.Name = "Constants";
constants.Size = sizeof(Constants);
constants.Usage = USAGE_DYNAMIC;
constants.BindFlags = BIND_UNIFORM_BUFFER;
constants.CPUAccessFlags = CPU_ACCESS_WRITE;
device_->CreateBuffer(constants, nullptr, &constantBuffer_);
// IMVU's textures: bilinear, no mipmaps, wrapped or clamped per texture.
for (int wrap = 0; wrap < 4; ++wrap) {
SamplerDesc sampler;
sampler.MinFilter = FILTER_TYPE_LINEAR;
sampler.MagFilter = FILTER_TYPE_LINEAR;
sampler.MipFilter = FILTER_TYPE_POINT;
sampler.AddressU = (wrap & 1) != 0 ? TEXTURE_ADDRESS_WRAP : TEXTURE_ADDRESS_CLAMP;
sampler.AddressV = (wrap & 2) != 0 ? TEXTURE_ADDRESS_WRAP : TEXTURE_ADDRESS_CLAMP;
device_->CreateSampler(sampler, &samplers_[wrap]);
}
set_identity(constants_.world);
set_identity(constants_.view);
set_identity(constants_.projection);
const float identity[6] = {1, 0, 0, 1, 0, 0};
set_texture_transform(constants_.texTransform0, identity);
set_texture_transform(constants_.texTransform1, identity);
constants_.flags[2] = 127; // the client's alpha reference
}
void Scene::setShaderSource(std::string source)
{
shaderSource_ = std::move(source);
shaders_.clear();
pipelines_.clear();
}
bool Scene::execute(std::span<const std::byte> commands, int offsetX, bool draw)
{
offsetX_ = offsetX;
draw_ = draw;
bool drew = false;
protocol::Reader reader(commands);
while (const auto command = reader.next()) {
switch (command->op()) {
case Op::BeginFrame:
if (const auto begin = command->body<protocol::BeginFrame>()) {
onscreen_ = begin->offscreen == 0 && draw_;
if (onscreen_) {
ITextureView* target = swapChain_->GetCurrentBackBufferRTV();
context_->SetRenderTargets(1, &target, swapChain_->GetDepthBufferDSV(),
RESOURCE_STATE_TRANSITION_MODE_TRANSITION);
}
}
break;
case Op::EndFrame:
if (const auto end = command->body<protocol::EndFrame>()) {
lastSentAt_ = end->sentAt;
}
drew = drew || onscreen_;
onscreen_ = false;
break;
case Op::Clear:
if (const auto clear = command->body<protocol::Clear>(); clear && onscreen_) {
if ((clear->flags & 1) != 0) {
float color[4];
set_color(color, clear->color);
context_->ClearRenderTarget(swapChain_->GetCurrentBackBufferRTV(), color,
RESOURCE_STATE_TRANSITION_MODE_TRANSITION);
}
if ((clear->flags & 2) != 0) {
context_->ClearDepthStencil(swapChain_->GetDepthBufferDSV(), CLEAR_DEPTH_FLAG, 1.0f, 0,
RESOURCE_STATE_TRANSITION_MODE_TRANSITION);
}
}
break;
case Op::SetProjection:
if (const auto projection = command->body<protocol::SetProjection>()) {
setProjection(*projection);
}
break;
case Op::SetModel:
if (const auto model = command->body<protocol::SetTransform>()) {
set_transform(constants_.world, model->m);
}
break;
case Op::SetView:
if (const auto view = command->body<protocol::SetTransform>()) {
set_transform(constants_.view, view->m);
}
break;
case Op::SetTextures:
if (const auto textures = command->body<protocol::SetTextures>()) {
state_.texture0 = textures->texture0;
state_.texture1 = textures->texture1;
}
break;
case Op::SetState:
if (const auto state = command->body<protocol::SetState>()) {
setState(*state);
}
break;
case Op::SetLight:
if (const auto light = command->body<protocol::SetLight>(); light && light->index < 8) {
set_color(constants_.lightColor[light->index], light->color);
constants_.lightColor[light->index][3] = light->enabled != 0 ? 1.0f : 0.0f;
std::memcpy(constants_.lightVector[light->index], light->vector, sizeof(light->vector));
constants_.lightVector[light->index][3] = light->directional != 0 ? 1.0f : 0.0f;
}
break;
case Op::SetFog:
if (const auto fog = command->body<protocol::SetFog>()) {
constants_.fog[0] = fog->start;
constants_.fog[1] = fog->end;
set_color(constants_.fogColor, fog->color);
}
break;
case Op::SetTextureTransform:
if (const auto transform = command->body<protocol::SetTextureTransform>(); transform && transform->stage < 2) {
set_texture_transform(transform->stage == 0 ? constants_.texTransform0 : constants_.texTransform1,
transform->m);
}
break;
case Op::CreateTexture:
createTexture(*command);
break;
case Op::UpdateTexture:
updateTexture(*command);
break;
case Op::DestroyTexture:
if (const auto destroy = command->body<protocol::DestroyTexture>()) {
textures_.erase(destroy->id);
}
break;
case Op::CreateBuffer:
createBuffer(*command);
break;
case Op::UpdateBuffer:
updateBuffer(*command);
break;
case Op::DestroyBuffer:
if (const auto destroy = command->body<protocol::DestroyBuffer>()) {
buffers_.erase(destroy->id);
}
break;
case Op::DrawTriangles:
if (const auto triangles = command->body<protocol::DrawTriangles>(); triangles && onscreen_) {
drawTriangles(*triangles);
}
break;
case Op::DrawArrays:
if (onscreen_) {
drawArrays(*command);
}
break;
case Op::DrawPointSprites:
if (onscreen_) {
drawPointSprites(*command);
}
break;
}
}
return drew;
}
void Scene::setProjection(const protocol::SetProjection& projection)
{
std::memcpy(constants_.projection, projection.m, sizeof(constants_.projection));
constants_.viewport[0] = static_cast<float>(projection.width);
constants_.viewport[1] = static_cast<float>(projection.height);
if (!onscreen_) {
return;
}
// The client's projection covers its whole 3D view; the window shows part of it, from offsetX.
const SwapChainDesc& window = swapChain_->GetDesc();
Viewport viewport;
viewport.TopLeftX = static_cast<float>(-offsetX_);
viewport.TopLeftY = 0;
viewport.Width = static_cast<float>(projection.width);
viewport.Height = static_cast<float>(projection.height);
viewport.MinDepth = 0;
viewport.MaxDepth = 1;
context_->SetViewports(1, &viewport, window.Width, window.Height);
}
void Scene::setState(const protocol::SetState& state)
{
const bool on = state.value != 0;
switch (state.state) {
case protocol::State::DepthTest:
state_.depthTest = on;
break;
case protocol::State::DepthWrite:
state_.depthWrite = on;
break;
case protocol::State::Blending:
state_.blending = on;
break;
case protocol::State::BlendMode:
if (state.value >= 1 && state.value <= 4) {
state_.blendMode = state.value;
}
break;
case protocol::State::AlphaTest:
constants_.flags[1] = on ? 1.0f : 0.0f;
break;
case protocol::State::AlphaReference:
constants_.flags[2] = static_cast<float>(state.value);
break;
case protocol::State::Culling:
state_.culling = on;
break;
case protocol::State::NormalInverting:
state_.normalInverting = on;
break;
case protocol::State::Lighting:
constants_.flags[0] = on ? 1.0f : 0.0f;
break;
case protocol::State::Fog:
constants_.fog[2] = on ? 1.0f : 0.0f;
break;
case protocol::State::Ambient:
set_color(constants_.ambient, state.value);
break;
}
}
void Scene::createTexture(const protocol::Command& command)
{
const auto create = command.body<protocol::CreateTexture>();
const auto pixels = command.payload<protocol::CreateTexture>();
if (!create || create->width == 0 || create->height == 0
|| pixels.size() < std::size_t{create->width} * create->height * 4) {
return;
}
TextureDesc desc;
desc.Name = "Client texture";
desc.Type = RESOURCE_DIM_TEX_2D;
desc.Width = create->width;
desc.Height = create->height;
desc.MipLevels = 1;
desc.Format = TEX_FORMAT_BGRA8_UNORM; // 0xAARRGGBB in memory
desc.BindFlags = BIND_SHADER_RESOURCE;
desc.Usage = USAGE_DEFAULT;
TextureSubResData level;
level.pData = pixels.data();
level.Stride = std::uint64_t{create->width} * 4;
TextureData data;
data.pSubResources = &level;
data.NumSubresources = 1;
Texture texture{.width = create->width, .height = create->height, .flags = create->flags};
device_->CreateTexture(desc, &data, &texture.texture);
if (!texture.texture) {
return;
}
texture.texture->GetDefaultView(TEXTURE_VIEW_SHADER_RESOURCE)
->SetSampler(samplers_[((create->flags & 2) != 0 ? 1 : 0) | ((create->flags & 4) != 0 ? 2 : 0)]);
textures_[create->id] = std::move(texture);
}
void Scene::updateTexture(const protocol::Command& command)
{
const auto update = command.body<protocol::UpdateTexture>();
const auto pixels = command.payload<protocol::UpdateTexture>();
if (!update || pixels.size() < std::size_t{update->width} * update->height * 4) {
return;
}
const auto found = textures_.find(update->id);
if (found == textures_.end()) {
return;
}
Texture& texture = found->second;
if (texture.width != update->width || texture.height != update->height) {
// A different size: make it again, keeping its sampling.
protocol::Writer writer;
std::memcpy(writer.add(Op::CreateTexture,
protocol::CreateTexture{update->id, update->width, update->height, texture.flags},
pixels.size()),
pixels.data(), pixels.size());
protocol::Reader reader(writer.bytes());
createTexture(*reader.next());
return;
}
Box box;
box.MaxX = update->width;
box.MaxY = update->height;
TextureSubResData level;
level.pData = pixels.data();
level.Stride = std::uint64_t{update->width} * 4;
context_->UpdateTexture(texture.texture, 0, 0, box, level, RESOURCE_STATE_TRANSITION_MODE_TRANSITION,
RESOURCE_STATE_TRANSITION_MODE_TRANSITION);
}
void Scene::createBuffer(const protocol::Command& command)
{
const auto create = command.body<protocol::CreateBuffer>();
if (!create || create->count == 0 || create->elementSize == 0) {
return;
}
const std::size_t size = std::size_t{create->elementSize} * create->count;
const auto contents = command.payload<protocol::CreateBuffer>();
const bool filled = contents.size() >= size;
BufferDesc desc;
desc.Name = create->kind == protocol::BufferKind::Index ? "Client indices" : "Client vertices";
desc.Size = size;
desc.BindFlags = create->kind == protocol::BufferKind::Index ? BIND_INDEX_BUFFER : BIND_VERTEX_BUFFER;
desc.Usage = filled ? USAGE_IMMUTABLE : USAGE_DEFAULT; // dynamic buffers are filled by updates
BufferData data;
data.pData = contents.data();
data.DataSize = size;
Buffer buffer{.kind = create->kind, .elementSize = create->elementSize, .count = create->count};
device_->CreateBuffer(desc, filled ? &data : nullptr, &buffer.buffer);
if (buffer.buffer) {
buffers_[create->id] = std::move(buffer);
}
}
void Scene::updateBuffer(const protocol::Command& command)
{
const auto update = command.body<protocol::UpdateBuffer>();
const auto contents = command.payload<protocol::UpdateBuffer>();
if (!update) {
return;
}
const auto found = buffers_.find(update->id);
const std::size_t size = std::size_t{update->elementSize} * update->count;
if (found == buffers_.end() || contents.size() < size || found->second.elementSize != update->elementSize
|| found->second.count != update->count || found->second.buffer->GetDesc().Usage != USAGE_DEFAULT) {
return;
}
context_->UpdateBuffer(found->second.buffer, 0, size, contents.data(), RESOURCE_STATE_TRANSITION_MODE_TRANSITION);
}
ITextureView* Scene::textureView(std::uint32_t id)
{
const auto found = textures_.find(id);
return found == textures_.end() ? nullptr : found->second.texture->GetDefaultView(TEXTURE_VIEW_SHADER_RESOURCE);
}
void Scene::drawTriangles(const protocol::DrawTriangles& draw)
{
const auto buffer = [this](std::uint32_t id, protocol::BufferKind kind, std::uint32_t elementSize) -> Buffer* {
const auto found = buffers_.find(id);
if (id == 0 || found == buffers_.end() || found->second.kind != kind || found->second.elementSize != elementSize) {
return nullptr;
}
return &found->second;
};
Buffer* indices = buffer(draw.indices, protocol::BufferKind::Index, 2);
Buffer* positions = buffer(draw.positions, protocol::BufferKind::Vertex, 12);
if (indices == nullptr || positions == nullptr) {
return;
}
// Optional streams count only if they cover every vertex.
const auto optional = [&](std::uint32_t id, std::uint32_t elementSize) -> IBuffer* {
Buffer* found = buffer(id, protocol::BufferKind::Vertex, elementSize);
return found != nullptr && found->count >= positions->count ? found->buffer.RawPtr() : nullptr;
};
this->draw(indices->buffer, indices->count, positions->buffer, optional(draw.normals, 12),
optional(draw.texCoords, 8), optional(draw.colors, 4));
}
void Scene::drawArrays(const protocol::Command& command)
{
const auto arrays = command.body<protocol::DrawArrays>();
const auto payload = command.payload<protocol::DrawArrays>();
if (!arrays || arrays->vertexCount == 0 || arrays->indexCount == 0) {
return;
}
const std::size_t indexBytes = protocol::padded(std::size_t{arrays->indexCount} * 2);
const std::size_t vertices = arrays->vertexCount;
if (payload.size() < indexBytes + vertices * (12 + (arrays->hasNormals ? 12 : 0) + (arrays->hasTexCoords ? 8 : 0) +
(arrays->hasColors ? 4 : 0))) {
return;
}
const auto make = [this](const std::byte* data, std::size_t size, BIND_FLAGS bind) {
BufferDesc desc;
desc.Name = "Client arrays";
desc.Size = size;
desc.BindFlags = bind;
desc.Usage = USAGE_IMMUTABLE;
BufferData contents;
contents.pData = data;
contents.DataSize = size;
RefCntAutoPtr<IBuffer> buffer;
device_->CreateBuffer(desc, &contents, &buffer);
return buffer;
};
const std::byte* next = payload.data();
const auto indices = make(next, std::size_t{arrays->indexCount} * 2, BIND_INDEX_BUFFER);
next += indexBytes;
const auto positions = make(next, vertices * 12, BIND_VERTEX_BUFFER);
next += vertices * 12;
RefCntAutoPtr<IBuffer> normals, texCoords, colors;
if (arrays->hasNormals) {
normals = make(next, vertices * 12, BIND_VERTEX_BUFFER);
next += vertices * 12;
}
if (arrays->hasTexCoords) {
texCoords = make(next, vertices * 8, BIND_VERTEX_BUFFER);
next += vertices * 8;
}
if (arrays->hasColors) {
colors = make(next, vertices * 4, BIND_VERTEX_BUFFER);
}
draw(indices, arrays->indexCount, positions, normals, texCoords, colors);
}
void Scene::draw(IBuffer* indices, std::uint32_t indexCount, IBuffer* positions, IBuffer* normals, IBuffer* texCoords,
IBuffer* colors)
{
if (indices == nullptr || positions == nullptr || indexCount == 0) {
return;
}
const std::uint32_t streams =
(normals != nullptr ? hasNormals : 0) | (texCoords != nullptr ? hasTexCoords : 0) | (colors != nullptr ? hasColors : 0);
if (prepare(streams, false) == nullptr) {
return;
}
IBuffer* vertexBuffers[4];
std::uint32_t count = 0;
for (IBuffer* stream : {positions, normals, texCoords, colors}) {
if (stream != nullptr) {
vertexBuffers[count++] = stream;
}
}
context_->SetVertexBuffers(0, count, vertexBuffers, nullptr, RESOURCE_STATE_TRANSITION_MODE_TRANSITION,
SET_VERTEX_BUFFERS_FLAG_RESET);
context_->SetIndexBuffer(indices, 0, RESOURCE_STATE_TRANSITION_MODE_TRANSITION);
DrawIndexedAttribs attributes;
attributes.NumIndices = indexCount;
attributes.IndexType = VT_UINT16;
context_->DrawIndexed(attributes);
}
Scene::Pipeline* Scene::prepare(std::uint32_t streams, bool sprites)
{
// Like the client: no textures, the first, or both (the second only with a first).
ITextureView* texture0 = textureView(state_.texture0);
ITextureView* texture1 = texture0 != nullptr ? textureView(state_.texture1) : nullptr;
const std::uint32_t textures = texture0 == nullptr ? 0 : texture1 == nullptr ? 1 : 2;
Pipeline* selected = pipeline(streams, textures, sprites);
if (selected == nullptr) {
return nullptr;
}
void* mapped = nullptr;
context_->MapBuffer(constantBuffer_, MAP_WRITE, MAP_FLAG_DISCARD, mapped);
if (mapped == nullptr) {
return nullptr;
}
std::memcpy(mapped, &constants_, sizeof(constants_));
context_->UnmapBuffer(constantBuffer_, MAP_WRITE);
if (selected->texture0 != nullptr) {
selected->texture0->Set(texture0);
}
if (selected->texture1 != nullptr) {
selected->texture1->Set(texture1);
}
context_->SetPipelineState(selected->state);
context_->CommitShaderResources(selected->bindings, RESOURCE_STATE_TRANSITION_MODE_TRANSITION);
return selected;
}
void Scene::drawPointSprites(const protocol::Command& command)
{
const auto sprites = command.body<protocol::DrawPointSprites>();
const auto payload = command.payload<protocol::DrawPointSprites>();
if (!sprites || sprites->count == 0 || payload.size() < std::size_t{sprites->count} * 16) {
return;
}
BufferDesc desc;
desc.Name = "Client particles";
desc.Size = std::size_t{sprites->count} * 16;
desc.BindFlags = BIND_VERTEX_BUFFER;
desc.Usage = USAGE_IMMUTABLE;
BufferData contents;
contents.pData = payload.data();
contents.DataSize = desc.Size;
RefCntAutoPtr<IBuffer> buffer;
device_->CreateBuffer(desc, &contents, &buffer);
if (!buffer || prepare(0, true) == nullptr) {
return;
}
if (spritesLogged_ < 5) {
++spritesLogged_;
const Texture* texture = textures_.contains(state_.texture0) ? &textures_[state_.texture0] : nullptr;
log::info("particles: {}, texture {} {}x{} flags {}, blending {} mode {}, depth test {} write {}, alpha test {} "
"reference {}, lighting {}, fog {}, ambient ({:.2f} {:.2f} {:.2f}), texture transform [{} {} {} {} {} {}]",
sprites->count, state_.texture0, texture ? texture->width : 0, texture ? texture->height : 0,
texture ? texture->flags : 0, state_.blending, state_.blendMode, state_.depthTest, state_.depthWrite,
constants_.flags[1], constants_.flags[2], constants_.flags[0], constants_.fog[2], constants_.ambient[0],
constants_.ambient[1], constants_.ambient[2], constants_.texTransform0[0], constants_.texTransform0[1],
constants_.texTransform0[2], constants_.texTransform0[4], constants_.texTransform0[5],
constants_.texTransform0[6]);
// The first sprite: where it is, seen from the camera, and how big it's drawn.
float point[4] = {0, 0, 0, 1};
std::uint32_t color = 0;
std::memcpy(point, payload.data(), 12);
std::memcpy(&color, payload.data() + 12, 4);
const auto transform = [](const float (&p)[4], const float (&m)[16]) {
std::array<float, 4> out{};
for (int column = 0; column < 4; ++column) {
out[column] = p[0] * m[column] + p[1] * m[4 + column] + p[2] * m[8 + column] + p[3] * m[12 + column];
}
return out;
};
const auto world = transform(point, constants_.world);
const float worldPoint[4] = {world[0], world[1], world[2], world[3]};
const auto view = transform(worldPoint, constants_.view);
const float distance = std::sqrt(view[0] * view[0] + view[1] * view[1] + view[2] * view[2]);
log::info(" first sprite at ({:.2f} {:.2f} {:.2f}), color {:08x}, from the camera ({:.2f} {:.2f} {:.2f}): "
"distance {:.2f}, {:.1f} pixels across (viewport height {})",
point[0], point[1], point[2], color, view[0], view[1], view[2], distance,
std::clamp(constants_.viewport[1] / std::max(distance, 1e-4f), 1.0f, 2048.0f), constants_.viewport[1]);
}
IBuffer* buffers[] = {buffer};
context_->SetVertexBuffers(0, 1, buffers, nullptr, RESOURCE_STATE_TRANSITION_MODE_TRANSITION,
SET_VERTEX_BUFFERS_FLAG_RESET);
DrawAttribs attributes;
attributes.NumVertices = 4;
attributes.NumInstances = sprites->count;
context_->Draw(attributes);
}
const Scene::Shaders& Scene::shaders(std::uint32_t streams, std::uint32_t textures, bool sprites)
{
const std::uint32_t key = streams | (textures << 3) | (sprites ? 1u << 5 : 0);
if (const auto found = shaders_.find(key); found != shaders_.end()) {
return found->second;
}
const std::string source = std::string("#define HAS_NORMALS ") + ((streams & hasNormals) != 0 ? "1" : "0") +
"\n#define HAS_TEX_COORDS " + ((streams & hasTexCoords) != 0 ? "1" : "0") +
"\n#define HAS_COLORS " + ((streams & hasColors) != 0 ? "1" : "0") +
"\n#define TEXTURES " + std::to_string(textures) + "\n" + shaderSource_;
Shaders created;
ShaderCreateInfo info;
info.SourceLanguage = SHADER_SOURCE_LANGUAGE_HLSL;
info.Desc.UseCombinedTextureSamplers = true; // g_TextureN with g_TextureN_sampler, set from each texture's view
info.Source = source.c_str();
info.Desc.ShaderType = SHADER_TYPE_VERTEX;
info.Desc.Name = sprites ? "Fixed function particles (vertex)" : "Fixed function (vertex)";
info.EntryPoint = sprites ? "vs_sprite" : "vs_main";
device_->CreateShader(info, &created.vertex);
info.Desc.ShaderType = SHADER_TYPE_PIXEL;
info.Desc.Name = "Fixed function (pixel)";
info.EntryPoint = "ps_main";
device_->CreateShader(info, &created.pixel);
return shaders_[key] = std::move(created);
}
Scene::Pipeline* Scene::pipeline(std::uint32_t streams, std::uint32_t textures, bool sprites)
{
const std::uint32_t blend = state_.blending ? state_.blendMode : 0;
const bool culling = state_.culling && !sprites; // point sprites aren't culled
const std::uint32_t key = streams | (textures << 3) | (blend << 5) | (state_.depthTest ? 1u << 8 : 0) |
(state_.depthWrite ? 1u << 9 : 0) | (culling ? 1u << 10 : 0) |
(state_.normalInverting ? 1u << 11 : 0) | (sprites ? 1u << 12 : 0);
if (const auto found = pipelines_.find(key); found != pipelines_.end()) {
return found->second.state ? &found->second : nullptr;
}
Pipeline& entry = pipelines_[key];
const Shaders& compiled = shaders(streams, textures, sprites);
if (!compiled.vertex || !compiled.pixel) {
return nullptr;
}
GraphicsPipelineStateCreateInfo info;
info.PSODesc.Name = "Fixed function";
info.PSODesc.PipelineType = PIPELINE_TYPE_GRAPHICS;
const SwapChainDesc& window = swapChain_->GetDesc();
GraphicsPipelineDesc& graphics = info.GraphicsPipeline;
graphics.NumRenderTargets = 1;
graphics.RTVFormats[0] = window.ColorBufferFormat;
graphics.DSVFormat = window.DepthBufferFormat;
graphics.PrimitiveTopology = sprites ? PRIMITIVE_TOPOLOGY_TRIANGLE_STRIP : PRIMITIVE_TOPOLOGY_TRIANGLE_LIST;
// The client culls clockwise triangles, or counter-clockwise ones when normals are inverted (mirrored objects).
graphics.RasterizerDesc.CullMode = culling ? CULL_MODE_BACK : CULL_MODE_NONE;
graphics.RasterizerDesc.FrontCounterClockwise = !state_.normalInverting;
graphics.DepthStencilDesc.DepthEnable = state_.depthTest;
graphics.DepthStencilDesc.DepthWriteEnable = state_.depthWrite;
graphics.DepthStencilDesc.DepthFunc = COMPARISON_FUNC_LESS;
RenderTargetBlendDesc& blending = graphics.BlendDesc.RenderTargets[0];
blending.BlendEnable = blend != 0;
if (blend != 0) {
const BLEND_FACTOR source = blend == 1 || blend == 4 ? BLEND_FACTOR_SRC_ALPHA : BLEND_FACTOR_ONE;
const BLEND_FACTOR destination = blend == 2 || blend == 4 ? BLEND_FACTOR_ONE : BLEND_FACTOR_INV_SRC_ALPHA;
blending.SrcBlend = blending.SrcBlendAlpha = source;
blending.DestBlend = blending.DestBlendAlpha = destination;
blending.BlendOp = blending.BlendOpAlpha = BLEND_OPERATION_ADD;
}
LayoutElement elements[4];
std::uint32_t count = 0;
if (sprites) {
// One buffer of sprites, each a position and a color, read once per square (instance).
elements[0] = LayoutElement{0, 0, 3, VT_FLOAT32, False};
elements[1] = LayoutElement{3, 0, 4, VT_UINT8, True};
elements[1].RelativeOffset = 12;
for (LayoutElement& element : std::span(elements).first(2)) {
element.Stride = 16;
element.Frequency = INPUT_ELEMENT_FREQUENCY_PER_INSTANCE;
element.InstanceDataStepRate = 1;
}
count = 2;
} else {
elements[count] = LayoutElement{0, count, 3, VT_FLOAT32, False};
++count;
}
if (!sprites && (streams & hasNormals) != 0) {
elements[count] = LayoutElement{1, count, 3, VT_FLOAT32, False};
++count;
}
if (!sprites && (streams & hasTexCoords) != 0) {
elements[count] = LayoutElement{2, count, 2, VT_FLOAT32, False};
++count;
}
if (!sprites && (streams & hasColors) != 0) {
elements[count] = LayoutElement{3, count, 4, VT_UINT8, True};
++count;
}
graphics.InputLayout.LayoutElements = elements;
graphics.InputLayout.NumElements = count;
info.pVS = compiled.vertex;
info.pPS = compiled.pixel;
const ShaderResourceVariableDesc variables[] = {
{SHADER_TYPE_PIXEL, "g_Texture0", SHADER_RESOURCE_VARIABLE_TYPE_DYNAMIC},
{SHADER_TYPE_PIXEL, "g_Texture1", SHADER_RESOURCE_VARIABLE_TYPE_DYNAMIC},
};
info.PSODesc.ResourceLayout.DefaultVariableType = SHADER_RESOURCE_VARIABLE_TYPE_STATIC;
info.PSODesc.ResourceLayout.Variables = variables;
info.PSODesc.ResourceLayout.NumVariables = textures;
device_->CreateGraphicsPipelineState(info, &entry.state);
if (!entry.state) {
return nullptr;
}
for (const SHADER_TYPE stage : {SHADER_TYPE_VERTEX, SHADER_TYPE_PIXEL}) {
if (IShaderResourceVariable* constants = entry.state->GetStaticVariableByName(stage, "Constants")) {
constants->Set(constantBuffer_);
}
}
entry.state->CreateShaderResourceBinding(&entry.bindings, true);
if (textures >= 1) {
entry.texture0 = entry.bindings->GetVariableByName(SHADER_TYPE_PIXEL, "g_Texture0");
}
if (textures >= 2) {
entry.texture1 = entry.bindings->GetVariableByName(SHADER_TYPE_PIXEL, "g_Texture1");
}
return &entry;
}
} // namespace t5de