diff --git a/qrenderdoc/Styles/RDStyle/RDStyle.cpp b/qrenderdoc/Styles/RDStyle/RDStyle.cpp index 38263e84e5..acf5d2b96a 100644 --- a/qrenderdoc/Styles/RDStyle/RDStyle.cpp +++ b/qrenderdoc/Styles/RDStyle/RDStyle.cpp @@ -1450,9 +1450,25 @@ void RDStyle::drawPrimitive(PrimitiveElement element, const QStyleOption *opt, Q group = QPalette::Inactive; if(viewitem->state & QStyle::State_Selected) - p->fillRect(viewitem->rect, viewitem->palette.brush(group, QPalette::Highlight)); - else if(viewitem->backgroundBrush.style() != Qt::NoBrush) - p->fillRect(viewitem->rect, viewitem->backgroundBrush); + { + if(viewitem->backgroundBrush.style() != Qt::NoBrush) + { + p->fillRect(viewitem->rect, viewitem->backgroundBrush); + // If we have a custom color, use the selection color at half opacity over the custom color + QColor col = viewitem->palette.color(group, QPalette::Highlight); + col.setAlphaF(0.5f); + p->fillRect(viewitem->rect, QBrush(col)); + } + else + { + p->fillRect(viewitem->rect, viewitem->palette.brush(group, QPalette::Highlight)); + } + } + else + { + if(viewitem->backgroundBrush.style() != Qt::NoBrush) + p->fillRect(viewitem->rect, viewitem->backgroundBrush); + } return; } diff --git a/qrenderdoc/Widgets/Extended/RDTreeView.cpp b/qrenderdoc/Widgets/Extended/RDTreeView.cpp index 5715a25b8e..31c1ffdb0e 100644 --- a/qrenderdoc/Widgets/Extended/RDTreeView.cpp +++ b/qrenderdoc/Widgets/Extended/RDTreeView.cpp @@ -771,18 +771,11 @@ void RDTreeView::drawBranches(QPainter *painter, const QRect &rect, const QModel idx = idx.parent(); } - opt.rect = rect; - opt.rect.setWidth(depth * indentation()); + opt.rect = allLinesRect; opt.showDecorationSelected = true; + opt.backgroundBrush = index.data(Qt::BackgroundRole).value(); style()->drawPrimitive(QStyle::PE_PanelItemViewItem, &opt, painter, this); - QBrush back = index.data(Qt::BackgroundRole).value(); - - if(!selectionModel()->isSelected(index) && back.style() != Qt::NoBrush) - { - painter->fillRect(allLinesRect, back); - } - if(m_VisibleBranches) { QTreeView::drawBranches(painter, rect, index); @@ -821,12 +814,12 @@ void RDTreeView::drawBranches(QPainter *painter, const QRect &rect, const QModel { parent = parents.pop(); - back = parent.data(RDTreeView::TreeLineColorRole).value(); + QBrush line = parent.data(RDTreeView::TreeLineColorRole).value(); - if(back.style() != Qt::NoBrush) + if(line.style() != Qt::NoBrush) { // draw a centred pen vertically down the middle of branchRect - painter->setPen(QPen(QBrush(back), m_treeColorLineWidth)); + painter->setPen(QPen(line, m_treeColorLineWidth)); QPoint topCentre = QRect(branchRect).center(); QPoint bottomCentre = topCentre; diff --git a/qrenderdoc/Windows/PixelHistoryView.cpp b/qrenderdoc/Windows/PixelHistoryView.cpp index 64a7c23540..d719974285 100644 --- a/qrenderdoc/Windows/PixelHistoryView.cpp +++ b/qrenderdoc/Windows/PixelHistoryView.cpp @@ -39,6 +39,24 @@ struct EventTag Q_DECLARE_METATYPE(EventTag); +enum +{ + COL_EVENT, + COL_TEX_BEFORE, + COL_TEX_BEFORE_COLOR, + COL_SHADER_OUT, + COL_SHADER_OUT_COLOR, + COL_SHADER_OUT_2, + COL_SHADER_OUT_2_COLOR, + COL_BLEND_SRC, + COL_BLEND_SRC_COLOR, + COL_BLEND_DST, + COL_BLEND_DST_COLOR, + COL_TEX_AFTER, + COL_TEX_AFTER_COLOR, + COL_COUNT, +}; + class PixelHistoryItemModel : public QAbstractItemModel { public: @@ -71,6 +89,20 @@ class PixelHistoryItemModel : public QAbstractItemModel } } + static bool isColorColumn(int column) + { + switch(column) + { + case COL_TEX_BEFORE_COLOR: + case COL_SHADER_OUT_COLOR: + case COL_SHADER_OUT_2_COLOR: + case COL_BLEND_SRC_COLOR: + case COL_BLEND_DST_COLOR: + case COL_TEX_AFTER_COLOR: return true; + default: return false; + } + } + void setHistory(const rdcarray &history) { m_ModList.reserve(history.count()); @@ -144,7 +176,7 @@ class PixelHistoryItemModel : public QAbstractItemModel return 0; } - int columnCount(const QModelIndex &parent = QModelIndex()) const override { return 5; } + int columnCount(const QModelIndex &parent = QModelIndex()) const override { return COL_COUNT; } Qt::ItemFlags flags(const QModelIndex &index) const override { if(!index.isValid()) @@ -155,11 +187,36 @@ class PixelHistoryItemModel : public QAbstractItemModel QVariant headerData(int section, Qt::Orientation orientation, int role) const override { - if(orientation == Qt::Horizontal && role == Qt::DisplayRole && section == 0) - return lit("Event"); + if(orientation == Qt::Horizontal && role == Qt::DisplayRole) + { + QString result; + switch(section) + { + case COL_EVENT: result = lit("Event"); break; + case COL_TEX_BEFORE: + case COL_TEX_BEFORE_COLOR: result = lit("Tex Before"); break; + case COL_SHADER_OUT: + case COL_SHADER_OUT_COLOR: result = lit("Shader Out"); break; + case COL_SHADER_OUT_2: + case COL_SHADER_OUT_2_COLOR: result = lit("Shader Out 2"); break; + case COL_BLEND_SRC: + case COL_BLEND_SRC_COLOR: result = lit("Blend Source"); break; + case COL_BLEND_DST: + case COL_BLEND_DST_COLOR: result = lit("Blend Dest"); break; + case COL_TEX_AFTER: + case COL_TEX_AFTER_COLOR: result = lit("Tex After"); break; + } + if(isColorColumn(section)) + { + // Pad with spaces so that the text isn't visible in the headers, only on the column chooser + // (it wouldn't fit anyways) + result = lit(" ") + result + lit(" color"); + } + return result; + } // sizes for the colour previews - if(orientation == Qt::Horizontal && role == Qt::SizeHintRole && (section == 2 || section == 4)) + if(orientation == Qt::Horizontal && role == Qt::SizeHintRole && isColorColumn(section)) return QSize(18, 0); return QVariant(); @@ -172,7 +229,7 @@ class PixelHistoryItemModel : public QAbstractItemModel int col = index.column(); // preview columns - if(col == 2 || col == 4) + if(isColorColumn(col)) { if(role == Qt::SizeHintRole) return QSize(16, 0); @@ -180,7 +237,7 @@ class PixelHistoryItemModel : public QAbstractItemModel if(m_Loading) { - if(role == Qt::DisplayRole && col == 0) + if(role == Qt::DisplayRole && col == COL_EVENT) return tr("Loading..."); return QVariant(); @@ -190,7 +247,7 @@ class PixelHistoryItemModel : public QAbstractItemModel { QString uavName = IsD3D(m_Ctx.APIProps().pipelineType) ? lit("UAV") : lit("Storage"); // main text - if(col == 0) + if(col == COL_EVENT) { if(isEvent(index)) { @@ -291,62 +348,106 @@ class PixelHistoryItemModel : public QAbstractItemModel } } - // pre mod/shader out text - if(col == 1) + if(isEvent(index)) { - if(isEvent(index)) + // We can't provide meaningful shader out messages for events, only individual primitives. + // So, use the Tex Before value for all of them. + if(col == COL_TEX_BEFORE || col == COL_SHADER_OUT || col == COL_SHADER_OUT_2 || + col == COL_BLEND_SRC) { return tr("Tex Before\n\n") + modString(getMods(index).first().preMod); } - else + else if(col == COL_TEX_AFTER || col == COL_BLEND_DST) + { + return tr("Tex After\n\n") + modString(getMods(index).last().postMod); + } + } + else + { + if(col == COL_TEX_BEFORE) + { + return tr("Tex Before\n\n") + modString(getMod(index).preMod); + } + else if(col == COL_SHADER_OUT || col == COL_SHADER_OUT_2) { const PixelModification &mod = getMod(index); if(mod.unboundPS) return tr("No Pixel\nShader\nBound"); if(mod.directShaderWrite) return tr("Tex Before\n\n") + modString(mod.preMod); - return tr("Shader Out\n\n") + modString(mod.shaderOut, 4); - } - } - // post mod text - if(col == 3) - { - if(isEvent(index)) - return tr("Tex After\n\n") + modString(getMods(index).last().postMod); - else + if(col == COL_SHADER_OUT) + return tr("Shader Out\n\n") + modString(mod.shaderOut, 4); + else + return tr("Shader Out 2\n\n") + modString(mod.shaderOutDualSrc, 4); + } + else if(col == COL_BLEND_SRC) + { + return tr("Blend Source\n\n") + modString(getMod(index).blendSrc); + } + else if(col == COL_BLEND_DST) + { + return tr("Blend Dest\n\n") + modString(getMod(index).blendDst); + } + else if(col == COL_TEX_AFTER) + { return tr("Tex After\n\n") + modString(getMod(index).postMod); + } } } if(role == Qt::BackgroundRole && (m_IsDepth || m_IsFloat)) { - // pre mod color - if(col == 2) + if(isEvent(index)) { - if(isEvent(index)) + if(col == COL_TEX_BEFORE_COLOR || col == COL_SHADER_OUT_COLOR || + col == COL_SHADER_OUT_2_COLOR || col == COL_BLEND_SRC_COLOR) { return backgroundBrush(getMods(index).first().preMod); } - else + else if(col == COL_TEX_AFTER_COLOR || col == COL_BLEND_DST_COLOR) + { + return backgroundBrush(getMods(index).last().postMod); + } + } + else + { + if(col == COL_TEX_BEFORE_COLOR) + { + return backgroundBrush(getMod(index).preMod); + } + else if(col == COL_SHADER_OUT_COLOR || col == COL_SHADER_OUT_2_COLOR) { const PixelModification &mod = getMod(index); if(mod.directShaderWrite) return backgroundBrush(mod.preMod); - return backgroundBrush(mod.shaderOut); + + if(col == COL_SHADER_OUT_COLOR) + return backgroundBrush(mod.shaderOut); + else + return backgroundBrush(mod.shaderOutDualSrc); } - } - else if(col == 4) - { - if(isEvent(index)) - return backgroundBrush(getMods(index).last().postMod); - else + else if(col == COL_BLEND_SRC_COLOR) + { + return backgroundBrush(getMod(index).blendSrc); + } + else if(col == COL_BLEND_DST_COLOR) + { + return backgroundBrush(getMod(index).blendDst); + } + else if(col == COL_TEX_AFTER_COLOR) + { return backgroundBrush(getMod(index).postMod); + } } } + const QColor GRAY_BACKGROUND = QColor::fromRgb(235, 235, 235); + const QColor GREEN_BACKGROUND = QColor::fromRgb(235, 255, 235); + const QColor RED_BACKGROUND = QColor::fromRgb(255, 235, 235); + // text backgrounds marking pass/fail - if(role == Qt::BackgroundRole && (col == 0 || col == 1 || col == 3)) + if(role == Qt::BackgroundRole && !isColorColumn(col)) { // rest if(isEvent(index)) @@ -359,24 +460,22 @@ class PixelHistoryItemModel : public QAbstractItemModel if(mods[0].directShaderWrite && mods[0].preMod.col.uintValue == mods[0].postMod.col.uintValue) - return QBrush(QColor::fromRgb(235, 235, 235)); + return QBrush(GRAY_BACKGROUND); - return passed ? QBrush(QColor::fromRgb(235, 255, 235)) - : QBrush(QColor::fromRgb(255, 235, 235)); + return passed ? QBrush(GREEN_BACKGROUND) : QBrush(RED_BACKGROUND); } else { if(!getMod(index).Passed()) - return QBrush(QColor::fromRgb(255, 235, 235)); + return QBrush(RED_BACKGROUND); } } // Since we change the background color for some cells, also change the foreground color to // ensure contrast with all UI themes - if(role == Qt::ForegroundRole && (col == 0 || col == 1 || col == 3)) + if(role == Qt::ForegroundRole && !isColorColumn(col)) { - QColor textColor = - contrastingColor(QColor::fromRgb(235, 235, 235), m_Palette.color(QPalette::Text)); + QColor textColor = contrastingColor(GRAY_BACKGROUND, m_Palette.color(QPalette::Text)); if(isEvent(index)) { return QBrush(textColor); @@ -639,11 +738,36 @@ PixelHistoryView::PixelHistoryView(ICaptureContext &ctx, ResourceId id, QPoint p ui->events->hideBranches(); - ui->events->header()->setSectionResizeMode(0, QHeaderView::Stretch); - ui->events->header()->setSectionResizeMode(1, QHeaderView::ResizeToContents); - ui->events->header()->setSectionResizeMode(2, QHeaderView::ResizeToContents); - ui->events->header()->setSectionResizeMode(3, QHeaderView::ResizeToContents); - ui->events->header()->setSectionResizeMode(4, QHeaderView::ResizeToContents); + ui->events->header()->setSectionResizeMode(COL_EVENT, QHeaderView::Stretch); + ui->events->header()->setSectionResizeMode(COL_TEX_BEFORE, QHeaderView::ResizeToContents); + ui->events->header()->setSectionResizeMode(COL_TEX_BEFORE_COLOR, QHeaderView::ResizeToContents); + ui->events->header()->setSectionResizeMode(COL_SHADER_OUT, QHeaderView::ResizeToContents); + ui->events->header()->setSectionResizeMode(COL_SHADER_OUT_COLOR, QHeaderView::ResizeToContents); + ui->events->header()->setSectionResizeMode(COL_SHADER_OUT_2, QHeaderView::ResizeToContents); + ui->events->header()->setSectionResizeMode(COL_SHADER_OUT_2_COLOR, QHeaderView::ResizeToContents); + ui->events->header()->setSectionResizeMode(COL_BLEND_SRC, QHeaderView::ResizeToContents); + ui->events->header()->setSectionResizeMode(COL_BLEND_SRC_COLOR, QHeaderView::ResizeToContents); + ui->events->header()->setSectionResizeMode(COL_BLEND_DST, QHeaderView::ResizeToContents); + ui->events->header()->setSectionResizeMode(COL_BLEND_DST_COLOR, QHeaderView::ResizeToContents); + ui->events->header()->setSectionResizeMode(COL_TEX_AFTER, QHeaderView::ResizeToContents); + ui->events->header()->setSectionResizeMode(COL_TEX_AFTER_COLOR, QHeaderView::ResizeToContents); + + // Hide these by default; only shader out and tex after are visible by default + ui->events->header()->hideSection(COL_TEX_BEFORE); + ui->events->header()->hideSection(COL_TEX_BEFORE_COLOR); + ui->events->header()->hideSection(COL_SHADER_OUT_2); + ui->events->header()->hideSection(COL_SHADER_OUT_2_COLOR); + ui->events->header()->hideSection(COL_BLEND_SRC); + ui->events->header()->hideSection(COL_BLEND_SRC_COLOR); + ui->events->header()->hideSection(COL_BLEND_DST); + ui->events->header()->hideSection(COL_BLEND_DST_COLOR); + + QObject::connect(ui->events, &RDTreeView::customContextMenuRequested, this, + &PixelHistoryView::events_contextMenu); + + ui->events->header()->setContextMenuPolicy(Qt::CustomContextMenu); + QObject::connect(ui->events->header(), &QHeaderView::customContextMenuRequested, this, + &PixelHistoryView::events_contextMenu); m_Ctx.AddCaptureViewer(this); } @@ -802,8 +926,19 @@ void PixelHistoryView::jumpToPrimitive(EventTag tag) } } -void PixelHistoryView::on_events_customContextMenuRequested(const QPoint &pos) +void PixelHistoryView::colSelect_clicked() +{ + QStringList headers; + for(int i = 0; i < ui->events->model()->columnCount(); i++) + headers << ui->events->model()->headerData(i, Qt::Horizontal).toString(); + UpdateVisibleColumns(tr("Select Pixel History Columns"), COL_COUNT, ui->events->header(), headers); +} + +void PixelHistoryView::events_contextMenu(const QPoint &pos) { + // TODO: this uses coordinates that are supposed to be relative to the header as coordinates in + // the table, so right-clicking the header uses the first entry instead (same applies to + // EventBrowser.cpp) QModelIndex index = ui->events->indexAt(pos); QMenu contextMenu(this); @@ -819,6 +954,13 @@ void PixelHistoryView::on_events_customContextMenuRequested(const QPoint &pos) ui->eventsHidden->setVisible(!m_ShowFailures); }); + QAction selectCols(tr("&Select Columns..."), this); + + contextMenu.addAction(&selectCols); + selectCols.setIcon(Icons::timeline_marker()); + + QObject::connect(&selectCols, &QAction::triggered, this, &PixelHistoryView::colSelect_clicked); + if(!index.isValid()) { RDDialog::show(&contextMenu, ui->events->viewport()->mapToGlobal(pos)); @@ -848,8 +990,8 @@ void PixelHistoryView::on_events_customContextMenuRequested(const QPoint &pos) debugText = tr("&Debug Pixel (%1, %2) primitive %3 at Event %4") .arg(m_Pixel.x()) .arg(m_Pixel.y()) - .arg(tag.eventId) - .arg(tag.primitive); + .arg(tag.primitive) + .arg(tag.eventId); contextMenu.addAction(&jumpAction); } diff --git a/qrenderdoc/Windows/PixelHistoryView.h b/qrenderdoc/Windows/PixelHistoryView.h index e468c65b2a..a11b6a57f1 100644 --- a/qrenderdoc/Windows/PixelHistoryView.h +++ b/qrenderdoc/Windows/PixelHistoryView.h @@ -55,9 +55,12 @@ class PixelHistoryView : public QFrame, public IPixelHistoryView, public ICaptur void OnEventChanged(uint32_t eventId) override; private slots: // automatic slots - void on_events_customContextMenuRequested(const QPoint &pos); void on_events_doubleClicked(const QModelIndex &index); + // manual slots + void colSelect_clicked(); + void events_contextMenu(const QPoint &pos); + protected: void enterEvent(QEvent *event) override; void leaveEvent(QEvent *event) override; diff --git a/renderdoc/api/replay/data_types.h b/renderdoc/api/replay/data_types.h index 3dbb1f1b7f..2423a486d1 100644 --- a/renderdoc/api/replay/data_types.h +++ b/renderdoc/api/replay/data_types.h @@ -2100,12 +2100,13 @@ struct PixelModification { return eventId == o.eventId && directShaderWrite == o.directShaderWrite && unboundPS == o.unboundPS && fragIndex == o.fragIndex && primitiveID == o.primitiveID && - preMod == o.preMod && shaderOut == o.shaderOut && postMod == o.postMod && - sampleMasked == o.sampleMasked && backfaceCulled == o.backfaceCulled && - depthClipped == o.depthClipped && depthBoundsFailed == o.depthBoundsFailed && - viewClipped == o.viewClipped && scissorClipped == o.scissorClipped && - shaderDiscarded == o.shaderDiscarded && depthTestFailed == o.depthTestFailed && - stencilTestFailed == o.stencilTestFailed; + preMod == o.preMod && shaderOut == o.shaderOut && + shaderOutDualSrc == o.shaderOutDualSrc && blendSrc == o.blendSrc && + blendDst == o.blendDst && postMod == o.postMod && sampleMasked == o.sampleMasked && + backfaceCulled == o.backfaceCulled && depthClipped == o.depthClipped && + depthBoundsFailed == o.depthBoundsFailed && viewClipped == o.viewClipped && + scissorClipped == o.scissorClipped && shaderDiscarded == o.shaderDiscarded && + depthTestFailed == o.depthTestFailed && stencilTestFailed == o.stencilTestFailed; } bool operator<(const PixelModification &o) const { @@ -2123,6 +2124,12 @@ struct PixelModification return preMod < o.preMod; if(!(shaderOut == o.shaderOut)) return shaderOut < o.shaderOut; + if(!(shaderOutDualSrc == o.shaderOutDualSrc)) + return shaderOutDualSrc < o.shaderOutDualSrc; + if(!(blendSrc == o.blendSrc)) + return blendSrc < o.blendSrc; + if(!(blendDst == o.blendDst)) + return blendDst < o.blendDst; if(!(postMod == o.postMod)) return postMod < o.postMod; if(!(sampleMasked == o.sampleMasked)) @@ -2175,6 +2182,21 @@ pixel. :type: ModificationValue )"); ModificationValue shaderOut; + DOCUMENT(R"(The value that this fragment wrote from the pixel shader to the second output. + +:type: ModificationValue +)"); + ModificationValue shaderOutDualSrc; + DOCUMENT(R"(The source component in the blend equation for this fragment. + +:type: ModificationValue +)"); + ModificationValue blendSrc; + DOCUMENT(R"(The destination component in the blend equation for this fragment. + +:type: ModificationValue +)"); + ModificationValue blendDst; DOCUMENT(R"(The value of the texture after this fragment ran. :type: ModificationValue diff --git a/renderdoc/driver/d3d11/d3d11_pixelhistory.cpp b/renderdoc/driver/d3d11/d3d11_pixelhistory.cpp index 77f436b73f..efa940f10b 100644 --- a/renderdoc/driver/d3d11/d3d11_pixelhistory.cpp +++ b/renderdoc/driver/d3d11/d3d11_pixelhistory.cpp @@ -1940,6 +1940,9 @@ rdcarray D3D11Replay::PixelHistory(rdcarray event // data[3].x (depth) unused // fragments writing to the pixel in this event with original shader mod.shaderOut.col.intValue[1] = int32_t(data[3].y); + mod.shaderOutDualSrc.SetInvalid(); + mod.blendSrc.SetInvalid(); + mod.blendDst.SetInvalid(); } } @@ -2316,6 +2319,9 @@ rdcarray D3D11Replay::PixelHistory(rdcarray event byte *data = shadoutStoreData + sizeof(Vec4f) * pixstoreStride * offsettedSlot; memcpy(&history[h].shaderOut.col.uintValue[0], data, 4 * sizeof(float)); + history[h].shaderOutDualSrc.SetInvalid(); + history[h].blendSrc.SetInvalid(); + history[h].blendDst.SetInvalid(); // primitive ID is in the next slot after that memcpy(&history[h].primitiveID, data + sizeof(Vec4f), sizeof(uint32_t)); diff --git a/renderdoc/driver/gl/gl_pixelhistory.cpp b/renderdoc/driver/gl/gl_pixelhistory.cpp index 515608be3e..648f81805a 100644 --- a/renderdoc/driver/gl/gl_pixelhistory.cpp +++ b/renderdoc/driver/gl/gl_pixelhistory.cpp @@ -1303,6 +1303,9 @@ std::map QueryNumFragmentsByEvent( numFragments = history[i].shaderOut.stencil; history[i].shaderOut.stencil = history[i].postMod.stencil; } + history[i].shaderOutDualSrc.SetInvalid(); + history[i].blendSrc.SetInvalid(); + history[i].blendDst.SetInvalid(); eventFragments.emplace(modEvents[i].eventId, numFragments); @@ -1587,6 +1590,9 @@ void QueryShaderOutPerFragment(WrappedOpenGL *driver, GLReplay *replay, int(historyIndex - history.begin())); historyIndex->shaderOut.stencil = oldStencil; } + historyIndex->shaderOutDualSrc.SetInvalid(); + historyIndex->blendSrc.SetInvalid(); + historyIndex->blendDst.SetInvalid(); historyIndex++; } diff --git a/renderdoc/driver/vulkan/vk_debug.h b/renderdoc/driver/vulkan/vk_debug.h index 84c5b2f076..2e0e4cf3a9 100644 --- a/renderdoc/driver/vulkan/vk_debug.h +++ b/renderdoc/driver/vulkan/vk_debug.h @@ -49,8 +49,6 @@ struct VKMeshDisplayPipelines uint32_t secondaryStridePadding = 0; }; -struct VkCopyPixelParams; - struct PixelHistoryResources; class VulkanResourceManager; @@ -97,10 +95,10 @@ class VulkanDebugManager bool PixelHistorySetupResources(PixelHistoryResources &resources, VkImage targetImage, VkExtent3D extent, VkFormat format, VkSampleCountFlagBits samples, const Subresource &sub, uint32_t numEvents); + bool PixelHistorySetupPerFragResources(PixelHistoryResources &resources, uint32_t numEvents, + uint32_t numFragments); bool PixelHistoryDestroyResources(const PixelHistoryResources &resources); - void PixelHistoryCopyPixel(VkCommandBuffer cmd, VkCopyPixelParams &p, size_t offset); - VkImageLayout GetImageLayout(ResourceId image, VkImageAspectFlagBits aspect, uint32_t mip, uint32_t slice); diff --git a/renderdoc/driver/vulkan/vk_pixelhistory.cpp b/renderdoc/driver/vulkan/vk_pixelhistory.cpp index 500ef7b28a..edde480857 100644 --- a/renderdoc/driver/vulkan/vk_pixelhistory.cpp +++ b/renderdoc/driver/vulkan/vk_pixelhistory.cpp @@ -63,11 +63,17 @@ * * - Fourth callback: Per fragment callback (VulkanPixelHistoryPerFragmentCallback) * This callback is used to get per fragment data for each event and fragment (primitive ID, - * shader output value, post event value for each fragment). - * For each fragment the draw is replayed 3 times: + * shader output value, dual source shader output value, blending source/destination components, + * and post event value for each fragment). + * For each fragment the draw is replayed 8 times: * 1) with a fragment shader that outputs primitive ID only * 2) with blending OFF, to get shader output value - * 3) with blending ON, to get post modification value + * 3) with blending OFF and a modified shader, to get the output at index 1 (dual source) + * 4) with blending ON, but with the stencil configured to only draw previous fragments + * 5) with blending ON but the destination factor set to 0, to get the source blend component + * 6) with blending ON, but with the stencil configured to only draw previous fragments + * 7) with blending ON but the source factor set to 0, to get the destination blend component + * 8) with blending ON, to get post modification value * For each such replay we set the stencil reference to the fragment number and set the * stencil compare to equal, so it passes for that particular fragment only. * @@ -81,6 +87,25 @@ * * We slot the per frament data correctly accounting for the fragments that were discarded. * + * Dual Source: + * + * To capture the output for both output indices when dual source blending is in use, we need to + * patch the shader so that ouptut index 1 is directed to output index 0. This might seem as simple + * as iterating through all of the outputs, and then for each Index decoration, swapping 0 with 1. + * Unfortunately, this does not work because index 0 is implicitly used if there is no Index + * decoration, meaning index 1 would get swapped into index 0, but index 0 would not get swapped to + * index 1, so multiple outputs end up at index 0. + * + * Another possible implementation would be to swap the target of any Index decorations which use + * index 1 to those which use index 0. However, there's no guarantee that there are the same number + * of outputs decorated with index 1 as with index 0, because the Component decoration means that + * e.g. index 0 could be written separately as ra and gb while index 1 is written as one output. + * + * The current implementation first computes what index values are currently in use for each output, + * and then NOPing out all Index decorations and adding new ones with the correctly swapped ones. + * Care is also taken to ensure that built-in outputs (those with the BuiltIn decoration) aren't + * given an Index decoration. + * * Current Limitations: * * - Multiple subpasses @@ -100,6 +125,9 @@ */ #include +#include +#include +#include #include "driver/shaders/spirv/spirv_editor.h" #include "driver/shaders/spirv/spirv_op_helpers.h" #include "maths/formatpacking.h" @@ -228,6 +256,9 @@ struct PerFragmentInfo int32_t primitiveID; uint32_t padding[3]; PixelHistoryValue shaderOut; + PixelHistoryValue shaderOutDualSrc; + PixelHistoryValue blendSrc; + PixelHistoryValue blendDst; PixelHistoryValue postMod; }; @@ -314,14 +345,31 @@ struct PixelHistoryShaderCache if(it != m_ShaderReplacements.end()) return it->second; - VkShaderModule shaderModule = CreateShaderReplacement(shaderId, entryPoint, stage); + VkShaderModule shaderModule = CreateShaderReplacement(shaderId, entryPoint, stage, false); m_ShaderReplacements.insert(std::make_pair(shaderKey, shaderModule)); return shaderModule; } + // Returns a shader that is equivalent to the given shader, except that index 0 outputs + // are directed to index 1 and index 1 outputs are directed to index 0 (as long as both exist). + // The shader is also stripped of side effects as is done by GetShaderWithoutSideEffects. + VkShaderModule GetDualSrcSwappedShader(ResourceId shaderId, const rdcstr &entryPoint, + ShaderStage stage) + { + ShaderKey shaderKey = make_rdcpair(shaderId, entryPoint); + auto it = m_ShaderReplacementsDualSrc.find(shaderKey); + // Check if we processed this shader before. + if(it != m_ShaderReplacementsDualSrc.end()) + return it->second; + + VkShaderModule shaderModule = CreateShaderReplacement(shaderId, entryPoint, stage, true); + m_ShaderReplacementsDualSrc.insert(std::make_pair(shaderKey, shaderModule)); + return shaderModule; + } + private: VkShaderModule CreateShaderReplacement(ResourceId shaderId, const rdcstr &entryName, - ShaderStage stage) + ShaderStage stage, bool swapDualSrc) { const VulkanCreationInfo::ShaderModule &moduleInfo = m_pDriver->GetDebugManager()->GetShaderInfo(shaderId); @@ -343,6 +391,8 @@ struct PixelHistoryShaderCache // just insert VK_NULL_HANDLE to indicate that this shader has been processed. found = true; modified = StripShaderSideEffects(editor, entry.id); + if(swapDualSrc) + modified |= SwapOutputIndex(editor, entry.usedIds, 1); break; } } @@ -589,6 +639,185 @@ struct PixelHistoryShaderCache return modified; } + bool SwapOutputIndex(rdcspv::Editor &editor, const rdcarray usedIds, + uint32_t index_to_swap_with_zero) + { + if(index_to_swap_with_zero == 0) + { + // Swapping 0 with 0 does nothing interesting + return false; + } + + bool modified = false; + + std::set outputs; + for(const rdcspv::Variable var : editor.GetGlobals()) + { + if(var.storage == rdcspv::StorageClass::Output && usedIds.contains(var.id)) + { + outputs.insert(var.id); + } + } + + // First pass: build up maps for location and index. + // Also NOP out any existing index declarations (but don't flag the shader as modified + // while doing so, as we might still decide that it doesn't need to be modified + // afterwards) + std::map id_to_location; + std::unordered_multimap location_to_id; + std::map id_to_index; + + std::set builtins; + + for(rdcspv::Iter it = editor.Begin(rdcspv::Section::Annotations), + end = editor.End(rdcspv::Section::Annotations); + it < end; ++it) + { + if(it.opcode() == rdcspv::Op::Decorate) + { + rdcspv::OpDecorate dec(it); + const rdcspv::Id decorated_id = dec.target; + if(dec.decoration == rdcspv::Decoration::Location && outputs.count(decorated_id) > 0) + { + const uint32_t decorated_location = dec.decoration.location; + + const auto insert_result = + id_to_location.emplace(std::make_pair(decorated_id, decorated_location)); + if(!insert_result.second) + { + // Note: by definition insert_result.first->first == decorated_id + const uint32_t existing_location = insert_result.first->second; + RDCWARN("Variable %u is decorated with multiple Locations: was %u, is %u", + decorated_id.value(), existing_location, decorated_location); + } + else + { + location_to_id.emplace(std::make_pair(decorated_location, decorated_id)); + } + } + if(dec.decoration == rdcspv::Decoration::Index && outputs.count(decorated_id) > 0) + { + const uint32_t decorated_index = dec.decoration.index; + + const auto insert_result = + id_to_index.emplace(std::make_pair(decorated_id, decorated_index)); + if(!insert_result.second) + { + // Note: by definition insert_result.first->first == decorated_id + const uint32_t existing_index = insert_result.first->second; + RDCWARN("Variable %u is decorated with multiple Indexes: was %u, is %u", + decorated_id.value(), existing_index, decorated_index); + } + + it.nopRemove(); + } + if(dec.decoration == rdcspv::Decoration::BuiltIn && outputs.count(decorated_id) > 0) + { + builtins.insert(decorated_id); + } + } + } + + std::unordered_map> location_to_indexes; + for(const auto entry : location_to_id) + { + const uint32_t location = entry.first; + const rdcspv::Id id = entry.second; + + const auto index_itr = id_to_index.find(id); + // Index is allowed to be absent, in which case it is implicitly zero + const uint32_t index = (index_itr != id_to_index.end()) ? index_itr->second : 0; + + location_to_indexes[location].insert(index); + } + + std::set locations_to_swap; + for(const auto &entry : location_to_indexes) + { + // https://registry.khronos.org/OpenGL/specs/gl/GLSLangSpec.4.60.html#output-layout-qualifiers + // The GLSL spec says that "Compile-time errors may also be given if at compile time it is + // known the link will fail. [...] It is also a compile-time error if a fragment shader sets a + // layout index to less than 0 or greater than 1." This is enforced by glslang. However, the + // Vulkan spec does not make this requirement (nor does the SPIRV spec). + // Note also that glslang will still compile a shader that uses index 1 but not index 0. + const uint32_t location = entry.first; + const std::set &indexes = entry.second; + + if(indexes.empty() || indexes.count(0) == 0) + { + RDCWARN("Output location %u does not output anything to index 0", location); + continue; + } + + const uint32_t highest_index = *indexes.rbegin(); + if(highest_index != (indexes.size() - 1)) + { + RDCWARN( + "Indexes for output location %u has a gap in the used indexes (highest used: %u, but " + "should be %zu based on size)", + location, highest_index, indexes.size() - 1); + continue; + } + + if(index_to_swap_with_zero <= highest_index) + { + // This location contains index 0 and the index we want to swap, + // so we can safely do the swap. + locations_to_swap.insert(location); + } + } + + for(const rdcspv::Id output : outputs) + { + if(builtins.count(output) > 0) + { + // VUID-StandaloneSpirv-Location-04915: "The Location or Component decorations must not be + // used with BuiltIn" + const auto location_itr = id_to_location.find(output); + if(location_itr != id_to_location.end()) + { + RDCWARN("Variable %u is a BuiltIn but is decorated with Location %u", output.value(), + location_itr->second); + continue; + } + } + else + { + // VUID-StandaloneSpirv-Location-04916: "The Location decorations must be used on + // user-defined variables" (Sec 15.1.2: "The non-built-in variables listed by OpEntryPoint + // with the Input or Output storage class form the user-defined variable interface.") + const auto location_itr = id_to_location.find(output); + if(location_itr == id_to_location.end()) + { + RDCWARN("Variable %u is missing a Location decoration", output.value()); + continue; + } + + const uint32_t location = location_itr->second; + + const auto index_itr = id_to_index.find(output); + // Index is allowed to be absent, in which case it is implicitly zero + const uint32_t existing_index = (index_itr != id_to_index.end()) ? index_itr->second : 0; + + uint32_t new_index = existing_index; + + if(locations_to_swap.count(location) != 0) + { + if(existing_index == 0) + new_index = index_to_swap_with_zero; + else if(existing_index == index_to_swap_with_zero) + new_index = 0; + modified = true; + } + + editor.AddDecoration(rdcspv::OpDecorate( + output, rdcspv::DecorationParam(new_index))); + } + } + + return modified; + } + WrappedVulkan *m_pDriver; std::map m_FixedColFS; std::map m_PrimIDFS; @@ -596,6 +825,7 @@ struct PixelHistoryShaderCache // ShaderKey consists of original shader module ID and entry point name. typedef rdcpair ShaderKey; std::map m_ShaderReplacements; + std::map m_ShaderReplacementsDualSrc; GPUBuffer dummybuf; }; @@ -1378,7 +1608,7 @@ struct VulkanPixelHistoryCallback : public VulkanActionCallback return (rpInfo.subpasses.size() > 1); } - // Returns teh color attachment index that corresponds to the target image for + // Returns the color attachment index that corresponds to the target image for // pixel history. uint32_t GetColorAttachmentIndex(const VulkanRenderState &renderstate, uint32_t *framebufferIndex = NULL) @@ -2654,8 +2884,17 @@ struct VulkanPixelHistoryPerFragmentCallback : VulkanPixelHistoryCallback VkPipeline primitiveIdPipe; // Turn off blending. VkPipeline shaderOutPipe; + // Turn off blending, and swaps output indexes to get the dual source output. + VkPipeline shaderOutDualSrcPipe; + // Enable blending but zero out the destination factor + VkPipeline blendSrcPipe; + // Enable blending but zero out the source factor + VkPipeline blendDstPipe; // Enable blending to get post event values. VkPipeline postModPipe; + // Enable blending to get post event values, and configure stencil to only draw up to + // immediately before the current event. + VkPipeline redrawToBeforePipe; }; void PreDraw(uint32_t eid, VkCommandBuffer cmd) @@ -2719,9 +2958,14 @@ struct VulkanPixelHistoryPerFragmentCallback : VulkanPixelHistoryCallback state.scissors[i].extent = {2, 2}; } - VkPipeline pipesIter[2]; + VkPipeline pipesIter[3] = {}; + uint32_t outputOffsetIter[3] = {}; pipesIter[0] = pipes.primitiveIdPipe; + outputOffsetIter[0] = offsetof(struct PerFragmentInfo, primitiveID); pipesIter[1] = pipes.shaderOutPipe; + outputOffsetIter[1] = offsetof(struct PerFragmentInfo, shaderOut); + pipesIter[2] = pipes.shaderOutDualSrcPipe; + outputOffsetIter[2] = offsetof(struct PerFragmentInfo, shaderOutDualSrc); VkCopyPixelParams colourCopyParams = {}; colourCopyParams.srcImage = m_CallbackInfo.subImage; @@ -2750,12 +2994,13 @@ struct VulkanPixelHistoryPerFragmentCallback : VulkanPixelHistoryCallback // Get primitive ID and shader output value for each fragment. for(uint32_t f = 0; f < numFragmentsInEvent; f++) { - for(uint32_t i = 0; i < 2; i++) - { - uint32_t storeOffset = (fragsProcessed + f) * sizeof(PerFragmentInfo); + const uint32_t fragStoreOffset = (fragsProcessed + f) * sizeof(PerFragmentInfo); - VkMarkerRegion region(cmd, StringFormat::Fmt("Getting %s for %u", - i == 0 ? "primitive ID" : "shader output", eid)); + for(uint32_t i = 0; i < (m_HasDualSrc ? 3u : 2u); i++) + { + const uint32_t storeOffset = fragStoreOffset + outputOffsetIter[i]; + const char *type = i == 0 ? "primitive ID" : i == 1 ? "shader output" : "shader output 2"; + VkMarkerRegion region(cmd, StringFormat::Fmt("Getting %s for %u", type, eid)); if(i == 0 && !m_pDriver->GetDeviceEnabledFeatures().geometryShader) { @@ -2772,7 +3017,7 @@ struct VulkanPixelHistoryPerFragmentCallback : VulkanPixelHistoryCallback // without one of the pipelines (e.g. if there was a geometry shader in use and we can't // read primitive ID in the fragment shader) we can't continue. // technically we can if the geometry shader outs a primitive ID, but that is unlikely. - VkMarkerRegion::Set("Can't get primitive ID with geometry shader in use", cmd); + VkMarkerRegion::Set(StringFormat::Fmt("No replacement shader in pipesIter[%d]", i), cmd); ObjDisp(cmd)->CmdFillBuffer(Unwrap(cmd), Unwrap(m_CallbackInfo.dstBuffer), storeOffset, 16, ~0U); @@ -2838,9 +3083,9 @@ struct VulkanPixelHistoryPerFragmentCallback : VulkanPixelHistoryCallback m_pDriver->ReplayDraw(cmd, *action); state.EndRenderPass(cmd); - if(i == 1) + CopyImagePixel(cmd, colourCopyParams, storeOffset); + if(i != 0) { - storeOffset += offsetof(struct PerFragmentInfo, shaderOut); if(depthEnabled) { VkCopyPixelParams depthCopyParams = colourCopyParams; @@ -2851,7 +3096,6 @@ struct VulkanPixelHistoryPerFragmentCallback : VulkanPixelHistoryCallback storeOffset + offsetof(struct PixelHistoryValue, depth)); } } - CopyImagePixel(cmd, colourCopyParams, storeOffset); } } @@ -2894,6 +3138,99 @@ struct VulkanPixelHistoryPerFragmentCallback : VulkanPixelHistoryCallback } } + // Get the blend source and destination components + const ModificationValue &premod = m_EventPremods[eid]; + if(m_HasBlend) + { + for(uint32_t f = 0; f < numFragmentsInEvent; f++) + { + for(uint32_t i = 0; i < 2; i++) + { + const uint32_t offset = (i == 0) ? offsetof(struct PerFragmentInfo, blendSrc) + : offsetof(struct PerFragmentInfo, blendDst); + VkMarkerRegion::Begin(StringFormat::Fmt("Prepare getting blend %s for fragment %u in %u", + i == 0 ? "source" : "destination", f, eid)); + + // Apply all draws BEFORE this point + // It's obvious why this is needed for the destination component; but for the source + // component, it is needed if the source factor uses the destination color or alpha. + state.graphics.pipeline = GetResID(pipes.redrawToBeforePipe); + state.BeginRenderPassAndApplyState(m_pDriver, cmd, VulkanRenderState::BindGraphics, false); + + // Have to reset stencil. + VkClearAttachment stencilAtt = {}; + stencilAtt.aspectMask = VK_IMAGE_ASPECT_STENCIL_BIT; + VkClearRect rect = {}; + rect.rect.offset.x = m_CallbackInfo.x; + rect.rect.offset.y = m_CallbackInfo.y; + rect.rect.extent.width = 1; + rect.rect.extent.height = 1; + rect.baseArrayLayer = 0; + rect.layerCount = 1; + ObjDisp(cmd)->CmdClearAttachments(Unwrap(cmd), 1, &stencilAtt, 1, &rect); + + // Before starting the draw, initialize the pixel to the premodification value + // for this event, for both color and depth. + VkClearAttachment clearAtts[2] = {}; + + clearAtts[0].aspectMask = VK_IMAGE_ASPECT_COLOR_BIT; + clearAtts[0].colorAttachment = colorOutputIndex; + memcpy(clearAtts[0].clearValue.color.float32, premod.col.floatValue.data(), + sizeof(clearAtts[0].clearValue.color)); + + clearAtts[1].aspectMask = VK_IMAGE_ASPECT_DEPTH_BIT; + clearAtts[1].clearValue.depthStencil.depth = premod.depth; + + if(IsDepthOrStencilFormat(m_CallbackInfo.targetImageFormat)) + ObjDisp(cmd)->CmdClearAttachments(Unwrap(cmd), 1, clearAtts + 1, 1, &rect); + else + ObjDisp(cmd)->CmdClearAttachments(Unwrap(cmd), 2, clearAtts, 1, &rect); + + // Note: pipes.redrawToBeforePipe has the stencil compare op set to VK_COMPARE_OP_GREATER, + // so this reference is not inclusive + ObjDisp(cmd)->CmdSetStencilCompareMask(Unwrap(cmd), VK_STENCIL_FACE_FRONT_AND_BACK, 0xff); + ObjDisp(cmd)->CmdSetStencilWriteMask(Unwrap(cmd), VK_STENCIL_FACE_FRONT_AND_BACK, 0xff); + ObjDisp(cmd)->CmdSetStencilReference(Unwrap(cmd), VK_STENCIL_FACE_FRONT_AND_BACK, f); + + const ActionDescription *action = m_pDriver->GetAction(eid); + m_pDriver->ReplayDraw(cmd, *action); + + // Now actually draw the current one. Stencil op is set to VK_COMPARE_OP_EQUAL. + VkMarkerRegion::Set(StringFormat::Fmt("Getting blend %s for fragment %u in %u", + i == 0 ? "source" : "destination", f, eid), + cmd); + + state.graphics.pipeline = GetResID(i == 0 ? pipes.blendSrcPipe : pipes.blendDstPipe); + state.BindPipeline(m_pDriver, cmd, VulkanRenderState::BindGraphics, false); + + ObjDisp(cmd)->CmdClearAttachments(Unwrap(cmd), 1, &stencilAtt, 1, &rect); + ObjDisp(cmd)->CmdSetStencilCompareMask(Unwrap(cmd), VK_STENCIL_FACE_FRONT_AND_BACK, 0xff); + ObjDisp(cmd)->CmdSetStencilWriteMask(Unwrap(cmd), VK_STENCIL_FACE_FRONT_AND_BACK, 0xff); + ObjDisp(cmd)->CmdSetStencilReference(Unwrap(cmd), VK_STENCIL_FACE_FRONT_AND_BACK, f); + + m_pDriver->ReplayDraw(cmd, *action); + state.EndRenderPass(cmd); + + CopyImagePixel(cmd, colourCopyParams, + (fragsProcessed + f) * sizeof(PerFragmentInfo) + offset); + + // TODO: is this useful? + if(depthImage != VK_NULL_HANDLE) + { + VkCopyPixelParams depthCopyParams = colourCopyParams; + depthCopyParams.srcImage = depthImage; + depthCopyParams.srcImageLayout = depthLayout; + depthCopyParams.srcImageFormat = depthFormat; + CopyImagePixel(cmd, depthCopyParams, (fragsProcessed + f) * sizeof(PerFragmentInfo) + + offset + + offsetof(struct PixelHistoryValue, depth)); + } + + VkMarkerRegion::End(); + } + } + } + // use the original renderpass and framebuffer attachment, but ensure we have depth and stencil state.SetRenderPass(prevState.GetRenderPass()); state.SetFramebuffer(prevState.GetFramebuffer(), prevState.GetFramebufferAttachments()); @@ -2912,7 +3249,6 @@ struct VulkanPixelHistoryPerFragmentCallback : VulkanPixelHistoryCallback GetResID(m_CallbackInfo.targetImage), aspect, m_CallbackInfo.targetSubresource.mip, m_CallbackInfo.targetSubresource.slice); - const ModificationValue &premod = m_EventPremods[eid]; // For every fragment except the last one, retrieve post-modification // value. for(uint32_t f = 0; f < numFragmentsInEvent - 1; f++) @@ -3055,14 +3391,24 @@ struct VulkanPixelHistoryPerFragmentCallback : VulkanPixelHistoryCallback EventFlags eventFlags = m_pDriver->GetEventFlags(eid); VkShaderModule replacementShaders[5] = {}; + VkShaderModule replacementDualSourceFragmentShader = VK_NULL_HANDLE; // Clean shaders uint32_t numberOfStages = 5; for(size_t i = 0; i < numberOfStages; i++) { - if((eventFlags & PipeStageRWEventFlags(StageFromIndex(i))) != EventFlags::NoFlags) + ShaderStage stage = StageFromIndex(i); + if(m_pDriver->GetDeviceEnabledFeatures().dualSrcBlend && stage == ShaderStage::Fragment) + { + replacementDualSourceFragmentShader = m_ShaderCache->GetDualSrcSwappedShader( + p.shaders[i].module, p.shaders[i].entryPoint, p.shaders[i].stage); + } + bool rwInStage = (eventFlags & PipeStageRWEventFlags(stage)) != EventFlags::NoFlags; + if(rwInStage) + { replacementShaders[i] = m_ShaderCache->GetShaderWithoutSideEffects( p.shaders[i].module, p.shaders[i].entryPoint, p.shaders[i].stage); + } } for(uint32_t i = 0; i < pipeCreateInfo.stageCount; i++) { @@ -3082,19 +3428,32 @@ struct VulkanPixelHistoryPerFragmentCallback : VulkanPixelHistoryCallback pipeCreateInfo.renderPass = rp; + ds->front.compareOp = VK_COMPARE_OP_GREATER; + ds->back.compareOp = VK_COMPARE_OP_GREATER; + + // Note: this uses the modified renderpass, as we switch to a different pipeline with the + // modified renderpass + vkr = m_pDriver->vkCreateGraphicsPipelines(m_pDriver->GetDev(), VK_NULL_HANDLE, 1, + &pipeCreateInfo, NULL, &pipes.redrawToBeforePipe); + m_pDriver->CheckVkResult(vkr); + m_PipesToDestroy.push_back(pipes.redrawToBeforePipe); + + // Revert to the previous stencil op + ds->front.compareOp = VK_COMPARE_OP_EQUAL; + ds->back.compareOp = VK_COMPARE_OP_EQUAL; + VkPipelineColorBlendStateCreateInfo *cbs = (VkPipelineColorBlendStateCreateInfo *)pipeCreateInfo.pColorBlendState; - // Turn off blending so that we can get shader output values. - VkPipelineColorBlendAttachmentState *atts = - (VkPipelineColorBlendAttachmentState *)cbs->pAttachments; + rdcarray newAtts; + newAtts.resize(colorOutputIndex == cbs->attachmentCount ? cbs->attachmentCount + 1 + : cbs->attachmentCount); + memcpy(newAtts.data(), cbs->pAttachments, + cbs->attachmentCount * sizeof(VkPipelineColorBlendAttachmentState)); // Check if we need to add a new color attachment. if(colorOutputIndex == cbs->attachmentCount) { - newAtts.resize(cbs->attachmentCount + 1); - memcpy(newAtts.data(), cbs->pAttachments, - cbs->attachmentCount * sizeof(VkPipelineColorBlendAttachmentState)); VkPipelineColorBlendAttachmentState newAtt = {}; if(cbs->attachmentCount > 0) { @@ -3105,22 +3464,28 @@ struct VulkanPixelHistoryPerFragmentCallback : VulkanPixelHistoryCallback else { newAtt.blendEnable = VK_FALSE; - newAtt.srcColorBlendFactor = VK_BLEND_FACTOR_DST_COLOR; } - newAtts[cbs->attachmentCount] = newAtt; - cbs->attachmentCount = (uint32_t)newAtts.size(); - cbs->pAttachments = newAtts.data(); - - atts = newAtts.data(); + newAtts[colorOutputIndex] = newAtt; + cbs->logicOpEnable = VK_FALSE; } + cbs->attachmentCount = (uint32_t)newAtts.size(); + cbs->pAttachments = newAtts.data(); + + // Turn off blending so that we can get shader output values. for(uint32_t i = 0; i < cbs->attachmentCount; i++) { - atts[i].blendEnable = 0; - atts[i].colorWriteMask = VK_COLOR_COMPONENT_R_BIT | VK_COLOR_COMPONENT_G_BIT | - VK_COLOR_COMPONENT_B_BIT | VK_COLOR_COMPONENT_A_BIT; + if(newAtts[i].blendEnable) + m_HasBlend = true; + newAtts[i].blendEnable = VK_FALSE; + newAtts[i].colorWriteMask = VK_COLOR_COMPONENT_R_BIT | VK_COLOR_COMPONENT_G_BIT | + VK_COLOR_COMPONENT_B_BIT | VK_COLOR_COMPONENT_A_BIT; } + // Logic ops treat it as if blendEnable were false + if(cbs->logicOpEnable) + m_HasBlend = false; + { ds->depthBoundsTestEnable = VK_FALSE; ds->depthWriteEnable = VK_TRUE; @@ -3133,6 +3498,73 @@ struct VulkanPixelHistoryPerFragmentCallback : VulkanPixelHistoryCallback m_PipesToDestroy.push_back(pipes.shaderOutPipe); + if(replacementDualSourceFragmentShader != VK_NULL_HANDLE) + { + m_HasDualSrc = true; + rdcarray dual_src_stages = stages; + for(uint32_t i = 0; i < pipeCreateInfo.stageCount; i++) + { + if(dual_src_stages[i].stage == VK_SHADER_STAGE_FRAGMENT_BIT) + dual_src_stages[i].module = replacementDualSourceFragmentShader; + } + + pipeCreateInfo.pStages = dual_src_stages.data(); + + vkr = m_pDriver->vkCreateGraphicsPipelines(m_pDriver->GetDev(), VK_NULL_HANDLE, 1, + &pipeCreateInfo, NULL, &pipes.shaderOutDualSrcPipe); + m_pDriver->CheckVkResult(vkr); + + m_PipesToDestroy.push_back(pipes.shaderOutDualSrcPipe); + + pipeCreateInfo.pStages = stages.data(); + } + + if(m_HasBlend) + { + rdcarray blendModifiedAtts; + blendModifiedAtts.resize(newAtts.size()); + cbs->pAttachments = blendModifiedAtts.data(); + + // Get the source component (shader out * source factor) of the blend equation + for(uint32_t i = 0; i < cbs->attachmentCount; i++) + { + blendModifiedAtts[i] = newAtts[i]; + blendModifiedAtts[i].blendEnable = VK_TRUE; + // Without changing the op, VK_BLEND_OP_REVERSE_SUBTRACT would cause problems + blendModifiedAtts[i].colorBlendOp = VK_BLEND_OP_ADD; + blendModifiedAtts[i].dstColorBlendFactor = VK_BLEND_FACTOR_ZERO; + blendModifiedAtts[i].alphaBlendOp = VK_BLEND_OP_ADD; + blendModifiedAtts[i].dstAlphaBlendFactor = VK_BLEND_FACTOR_ZERO; + } + + vkr = m_pDriver->vkCreateGraphicsPipelines(m_pDriver->GetDev(), VK_NULL_HANDLE, 1, + &pipeCreateInfo, NULL, &pipes.blendSrcPipe); + m_pDriver->CheckVkResult(vkr); + + m_PipesToDestroy.push_back(pipes.blendSrcPipe); + + // Get the destination component (pre mod * destination factor) of the blend equation + for(uint32_t i = 0; i < cbs->attachmentCount; i++) + { + blendModifiedAtts[i] = newAtts[i]; + blendModifiedAtts[i].blendEnable = VK_TRUE; + // Without changing the op, VK_BLEND_OP_SUBTRACT would cause problems + blendModifiedAtts[i].colorBlendOp = VK_BLEND_OP_ADD; + blendModifiedAtts[i].srcColorBlendFactor = VK_BLEND_FACTOR_ZERO; + blendModifiedAtts[i].alphaBlendOp = VK_BLEND_OP_ADD; + blendModifiedAtts[i].srcAlphaBlendFactor = VK_BLEND_FACTOR_ZERO; + } + + vkr = m_pDriver->vkCreateGraphicsPipelines(m_pDriver->GetDev(), VK_NULL_HANDLE, 1, + &pipeCreateInfo, NULL, &pipes.blendDstPipe); + m_pDriver->CheckVkResult(vkr); + + m_PipesToDestroy.push_back(pipes.blendDstPipe); + + // Return to blending being disabled + cbs->pAttachments = newAtts.data(); + } + { ds->depthTestEnable = VK_FALSE; ds->depthWriteEnable = VK_FALSE; @@ -3212,6 +3644,9 @@ struct VulkanPixelHistoryPerFragmentCallback : VulkanPixelHistoryCallback return it->second; } + bool m_HasBlend = false; + bool m_HasDualSrc = false; + private: // For each event, specifies where the occlusion query results start. std::map m_EventIndices; @@ -3361,9 +3796,9 @@ bool VulkanDebugManager::PixelHistorySetupResources(PixelHistoryResources &resou VkFormat format, VkSampleCountFlagBits samples, const Subresource &sub, uint32_t numEvents) { - VkMarkerRegion region(StringFormat::Fmt("PixelHistorySetupResources %ux%ux%u %s %ux MSAA", - extent.width, extent.height, extent.depth, - ToStr(format).c_str(), samples)); + VkMarkerRegion region( + StringFormat::Fmt("PixelHistorySetupResources %ux%ux%u %s %ux MSAA, %u events", extent.width, + extent.height, extent.depth, ToStr(format).c_str(), samples, numEvents)); VulkanCreationInfo::Image targetImageInfo = GetImageInfo(GetResID(targetImage)); VkImage colorImage; @@ -3473,8 +3908,6 @@ bool VulkanDebugManager::PixelHistorySetupResources(PixelHistoryResources &resou CheckVkResult(vkr); VkBufferCreateInfo bufferInfo = {VK_STRUCTURE_TYPE_BUFFER_CREATE_INFO}; - // TODO: the size for memory is calculated to fit pre and post modification values and - // stencil values. But we might run out of space when getting per fragment data. bufferInfo.size = AlignUp((uint32_t)(numEvents * sizeof(EventInfo)), 4096U); bufferInfo.usage = VK_BUFFER_USAGE_TRANSFER_DST_BIT | VK_BUFFER_USAGE_STORAGE_BUFFER_BIT; @@ -3530,6 +3963,75 @@ bool VulkanDebugManager::PixelHistorySetupResources(PixelHistoryResources &resou return true; } +bool VulkanDebugManager::PixelHistorySetupPerFragResources(PixelHistoryResources &resources, + uint32_t numEvents, uint32_t numFrags) +{ + const uint32_t existingBufferSize = AlignUp((uint32_t)(numEvents * sizeof(EventInfo)), 4096U); + const uint32_t requiredBufferSize = AlignUp((uint32_t)(numFrags * sizeof(PerFragmentInfo)), 4096U); + + // If the existing buffer is big enough for all of the fragments, we can re-use it. + const bool canReuseBuffer = existingBufferSize >= requiredBufferSize; + + VkMarkerRegion region(StringFormat::Fmt( + "PixelHistorySetupPerFragResources %u events %u frags, buffer size %u -> %u, %s old buffer", + numEvents, numFrags, existingBufferSize, requiredBufferSize, + canReuseBuffer ? "reusing" : "NOT reusing")); + + if(canReuseBuffer) + return true; + + // Otherwise, destroy it and create a new one that's big enough in its place. + VkDevice dev = m_pDriver->GetDev(); + + if(resources.dstBuffer != VK_NULL_HANDLE) + m_pDriver->vkDestroyBuffer(dev, resources.dstBuffer, NULL); + if(resources.bufferMemory != VK_NULL_HANDLE) + m_pDriver->vkFreeMemory(dev, resources.bufferMemory, NULL); + resources.dstBuffer = VK_NULL_HANDLE; + resources.bufferMemory = VK_NULL_HANDLE; + + VkBufferCreateInfo bufferInfo = {VK_STRUCTURE_TYPE_BUFFER_CREATE_INFO}; + bufferInfo.size = requiredBufferSize; + bufferInfo.usage = VK_BUFFER_USAGE_TRANSFER_DST_BIT | VK_BUFFER_USAGE_STORAGE_BUFFER_BIT; + + VkResult vkr = m_pDriver->vkCreateBuffer(m_Device, &bufferInfo, NULL, &resources.dstBuffer); + CheckVkResult(vkr); + + // Allocate memory + VkMemoryRequirements mrq = {}; + m_pDriver->vkGetBufferMemoryRequirements(m_Device, resources.dstBuffer, &mrq); + VkMemoryAllocateInfo allocInfo = { + VK_STRUCTURE_TYPE_MEMORY_ALLOCATE_INFO, NULL, mrq.size, + m_pDriver->GetReadbackMemoryIndex(mrq.memoryTypeBits), + }; + vkr = m_pDriver->vkAllocateMemory(m_Device, &allocInfo, NULL, &resources.bufferMemory); + CheckVkResult(vkr); + + if(vkr != VK_SUCCESS) + return false; + + vkr = m_pDriver->vkBindBufferMemory(m_Device, resources.dstBuffer, resources.bufferMemory, 0); + CheckVkResult(vkr); + + VkCommandBuffer cmd = m_pDriver->GetNextCmd(); + VkCommandBufferBeginInfo beginInfo = {VK_STRUCTURE_TYPE_COMMAND_BUFFER_BEGIN_INFO, NULL, + VK_COMMAND_BUFFER_USAGE_ONE_TIME_SUBMIT_BIT}; + + if(cmd == VK_NULL_HANDLE) + return false; + + vkr = ObjDisp(dev)->BeginCommandBuffer(Unwrap(cmd), &beginInfo); + CheckVkResult(vkr); + ObjDisp(cmd)->CmdFillBuffer(Unwrap(cmd), Unwrap(resources.dstBuffer), 0, VK_WHOLE_SIZE, 0); + + vkr = ObjDisp(dev)->EndCommandBuffer(Unwrap(cmd)); + CheckVkResult(vkr); + m_pDriver->SubmitCmds(); + m_pDriver->FlushQ(); + + return true; +} + VkDescriptorSet VulkanReplay::GetPixelHistoryDescriptor() { VkDescriptorSet descSet; @@ -3972,6 +4474,9 @@ rdcarray VulkanReplay::PixelHistory(rdcarray even mod.preMod.SetInvalid(); mod.postMod.SetInvalid(); mod.shaderOut.SetInvalid(); + mod.shaderOutDualSrc.SetInvalid(); + mod.blendSrc.SetInvalid(); + mod.blendDst.SetInvalid(); h++; continue; } @@ -3999,6 +4504,9 @@ rdcarray VulkanReplay::PixelHistory(rdcarray even int32_t fragsClipped = int32_t(ei.dsWithShaderDiscard[4]); mod.shaderOut.col.intValue[0] = frags; mod.shaderOut.col.intValue[1] = fragsClipped; + mod.shaderOutDualSrc.SetInvalid(); + mod.blendSrc.SetInvalid(); + mod.blendDst.SetInvalid(); bool someFragsClipped = (fragsClipped < frags); mod.primitiveID = someFragsClipped; // Draws in secondary command buffers will fail this check, @@ -4029,6 +4537,17 @@ rdcarray VulkanReplay::PixelHistory(rdcarray even if(eventsWithFrags.size() > 0) { + uint32_t numFrags = 0; + for(auto &item : eventsWithFrags) + { + numFrags += item.second; + } + + GetDebugManager()->PixelHistorySetupPerFragResources(resources, (uint32_t)events.size(), + numFrags); + + callbackInfo.dstBuffer = resources.dstBuffer; + // Replay to get shader output value, post modification value and primitive ID for every // fragment. VulkanPixelHistoryPerFragmentCallback perFragmentCB(m_pDriver, shaderCache, callbackInfo, @@ -4145,6 +4664,22 @@ rdcarray VulkanReplay::PixelHistory(rdcarray even { history[h].preMod = history[h - 1].postMod; } + + if(perFragmentCB.m_HasDualSrc) + FillInColor(shaderOutFormat, bp[offset].shaderOutDualSrc, history[h].shaderOutDualSrc); + else + history[h].shaderOutDualSrc.SetInvalid(); + + if(perFragmentCB.m_HasBlend) + { + FillInColor(shaderOutFormat, bp[offset].blendSrc, history[h].blendSrc); + FillInColor(shaderOutFormat, bp[offset].blendDst, history[h].blendDst); + } + else + { + history[h].blendSrc.SetInvalid(); + history[h].blendDst.SetInvalid(); + } } // check the depth value between premod/shaderout against the known test if we have valid @@ -4194,6 +4729,8 @@ rdcarray VulkanReplay::PixelHistory(rdcarray even history[h].depthBoundsFailed = true; } } + + m_pDriver->vkUnmapMemory(dev, resources.bufferMemory); } SAFE_DELETE(tfCb); diff --git a/renderdoc/replay/renderdoc_serialise.inl b/renderdoc/replay/renderdoc_serialise.inl index 058149e3bb..4fd593c975 100644 --- a/renderdoc/replay/renderdoc_serialise.inl +++ b/renderdoc/replay/renderdoc_serialise.inl @@ -893,6 +893,9 @@ void DoSerialise(SerialiserType &ser, PixelModification &el) SERIALISE_MEMBER(preMod); SERIALISE_MEMBER(shaderOut); + SERIALISE_MEMBER(shaderOutDualSrc); + SERIALISE_MEMBER(blendSrc); + SERIALISE_MEMBER(blendDst); SERIALISE_MEMBER(postMod); SERIALISE_MEMBER(sampleMasked); @@ -906,7 +909,7 @@ void DoSerialise(SerialiserType &ser, PixelModification &el) SERIALISE_MEMBER(stencilTestFailed); SERIALISE_MEMBER(predicationSkipped); - SIZE_CHECK(100); + SIZE_CHECK(172); } template diff --git a/util/test/demos/CMakeLists.txt b/util/test/demos/CMakeLists.txt index 108bf6d083..80fd454306 100644 --- a/util/test/demos/CMakeLists.txt +++ b/util/test/demos/CMakeLists.txt @@ -87,6 +87,7 @@ set(VULKAN_SRC vk/vk_test.cpp vk/vk_test.h vk/vk_adv_cbuffer_zoo.cpp + vk/vk_blend.cpp vk/vk_buffer_truncation.cpp vk/vk_cbuffer_zoo.cpp vk/vk_compute_only.cpp @@ -98,6 +99,7 @@ set(VULKAN_SRC vk/vk_discard_rects.cpp vk/vk_discard_zoo.cpp vk/vk_draw_zoo.cpp + vk/vk_dual_source.cpp vk/vk_dynamic_rendering.cpp vk/vk_empty_capture.cpp vk/vk_ext_buffer_address.cpp diff --git a/util/test/demos/demos.vcxproj b/util/test/demos/demos.vcxproj index 19b54ca8fd..5ef7b36956 100644 --- a/util/test/demos/demos.vcxproj +++ b/util/test/demos/demos.vcxproj @@ -296,12 +296,14 @@ + + diff --git a/util/test/demos/demos.vcxproj.filters b/util/test/demos/demos.vcxproj.filters index d3cc6524c0..798331d145 100644 --- a/util/test/demos/demos.vcxproj.filters +++ b/util/test/demos/demos.vcxproj.filters @@ -673,6 +673,12 @@ D3D12\demos + + Vulkan\demos + + + Vulkan\demos + diff --git a/util/test/demos/vk/vk_blend.cpp b/util/test/demos/vk/vk_blend.cpp new file mode 100644 index 0000000000..232343c7af --- /dev/null +++ b/util/test/demos/vk/vk_blend.cpp @@ -0,0 +1,246 @@ +/****************************************************************************** + * The MIT License (MIT) + * + * Copyright (c) 2019-2023 Baldur Karlsson + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + ******************************************************************************/ + +#include "vk_test.h" + +RD_TEST(VK_Blend, VulkanGraphicsTest) +{ + static constexpr const char *Description = + "Draws a triangle repeatedly to test blending within a single drawcall"; + + const DefaultA2V TemplateTriangleRed[3] = { + {Vec3f(-0.5f, -0.5f, 0.0f), Vec4f(1 / 255.f, 0.0f, 0.0f, 1.0f), Vec2f(0.0f, 0.0f)}, + {Vec3f(0.0f, 0.5f, 0.0f), Vec4f(1 / 255.f, 0.0f, 0.0f, 1.0f), Vec2f(0.0f, 1.0f)}, + {Vec3f(0.5f, -0.5f, 0.0f), Vec4f(1 / 255.f, 0.0f, 0.0f, 1.0f), Vec2f(1.0f, 0.0f)}, + }; + const int TRIANGLES_RED_INDEX = 0; + const int NUM_TRIANGLES_RED = 16; + const DefaultA2V TemplateTriangleGreen[3] = { + {Vec3f(-0.5f, -0.5f, 0.0f), Vec4f(0.0f, 1 / 255.f, 0.0f, 1.0f), Vec2f(0.0f, 0.0f)}, + {Vec3f(0.0f, 0.5f, 0.0f), Vec4f(0.0f, 1 / 255.f, 0.0f, 1.0f), Vec2f(0.0f, 1.0f)}, + {Vec3f(0.5f, -0.5f, 0.0f), Vec4f(0.0f, 1 / 255.f, 0.0f, 1.0f), Vec2f(1.0f, 0.0f)}, + }; + const int TRIANGLES_GREEN_INDEX = TRIANGLES_RED_INDEX + NUM_TRIANGLES_RED; + const int NUM_TRIANGLES_GREEN = 255; + const DefaultA2V TemplateTriangleBlue[3] = { + {Vec3f(-0.5f, -0.5f, 0.0f), Vec4f(0.0f, 0.0f, 1 / 255.f, 1.0f), Vec2f(0.0f, 0.0f)}, + {Vec3f(0.0f, 0.5f, 0.0f), Vec4f(0.0f, 0.0f, 1 / 255.f, 1.0f), Vec2f(0.0f, 1.0f)}, + {Vec3f(0.5f, -0.5f, 0.0f), Vec4f(0.0f, 0.0f, 1 / 255.f, 1.0f), Vec2f(1.0f, 0.0f)}, + }; + const int TRIANGLES_BLUE_INDEX = TRIANGLES_GREEN_INDEX + NUM_TRIANGLES_GREEN; + const int NUM_TRIANGLES_BLUE = 512; + + const int NUM_TRIANGLES_TOTAL = TRIANGLES_BLUE_INDEX + NUM_TRIANGLES_BLUE; + + int main() + { + // initialise, create window, create context, etc + requestedSwapChainFormat = VK_FORMAT_B8G8R8A8_UNORM; + if(!Init()) + return 3; + + VkPipelineLayout layout = createPipelineLayout(vkh::PipelineLayoutCreateInfo()); + + vkh::GraphicsPipelineCreateInfo pipeCreateInfo; + + pipeCreateInfo.layout = layout; + pipeCreateInfo.renderPass = mainWindow->rp; + + VkPipelineColorBlendAttachmentState colorBlendAttachment{}; + colorBlendAttachment.colorWriteMask = VK_COLOR_COMPONENT_R_BIT | VK_COLOR_COMPONENT_G_BIT | + VK_COLOR_COMPONENT_B_BIT | VK_COLOR_COMPONENT_A_BIT; + colorBlendAttachment.blendEnable = VK_TRUE; + colorBlendAttachment.srcColorBlendFactor = VK_BLEND_FACTOR_ONE; + colorBlendAttachment.dstColorBlendFactor = VK_BLEND_FACTOR_ONE; + colorBlendAttachment.colorBlendOp = VK_BLEND_OP_ADD; + colorBlendAttachment.srcAlphaBlendFactor = VK_BLEND_FACTOR_ONE; + colorBlendAttachment.dstAlphaBlendFactor = VK_BLEND_FACTOR_ZERO; + colorBlendAttachment.alphaBlendOp = VK_BLEND_OP_ADD; + pipeCreateInfo.colorBlendState.attachments = {colorBlendAttachment}; + + pipeCreateInfo.vertexInputState.vertexBindingDescriptions = {vkh::vertexBind(0, DefaultA2V)}; + pipeCreateInfo.vertexInputState.vertexAttributeDescriptions = { + vkh::vertexAttr(0, 0, DefaultA2V, pos), vkh::vertexAttr(1, 0, DefaultA2V, col), + vkh::vertexAttr(2, 0, DefaultA2V, uv), + }; + + pipeCreateInfo.stages = { + CompileShaderModule(VKDefaultVertex, ShaderLang::glsl, ShaderStage::vert, "main"), + CompileShaderModule(VKDefaultPixel, ShaderLang::glsl, ShaderStage::frag, "main"), + }; + + VkPipeline pipe = createGraphicsPipeline(pipeCreateInfo); + + std::vector triangles; + triangles.reserve(3 * NUM_TRIANGLES_TOTAL); + for(int i = 0; i < NUM_TRIANGLES_RED; i++) + { + triangles.push_back(TemplateTriangleRed[0]); + triangles.push_back(TemplateTriangleRed[1]); + triangles.push_back(TemplateTriangleRed[2]); + } + for(int i = 0; i < NUM_TRIANGLES_GREEN; i++) + { + triangles.push_back(TemplateTriangleGreen[0]); + triangles.push_back(TemplateTriangleGreen[1]); + triangles.push_back(TemplateTriangleGreen[2]); + } + for(int i = 0; i < NUM_TRIANGLES_BLUE; i++) + { + triangles.push_back(TemplateTriangleBlue[0]); + triangles.push_back(TemplateTriangleBlue[1]); + triangles.push_back(TemplateTriangleBlue[2]); + } + + AllocatedBuffer vb(this, vkh::BufferCreateInfo(sizeof(DefaultA2V) * 3 * NUM_TRIANGLES_TOTAL, + VK_BUFFER_USAGE_VERTEX_BUFFER_BIT | + VK_BUFFER_USAGE_TRANSFER_DST_BIT), + VmaAllocationCreateInfo({0, VMA_MEMORY_USAGE_CPU_TO_GPU})); + + vb.upload(triangles.data(), sizeof(DefaultA2V) * 3 * NUM_TRIANGLES_TOTAL); + + AllocatedImage offimg(this, vkh::ImageCreateInfo(4, 4, 0, VK_FORMAT_R32G32B32A32_SFLOAT, + VK_IMAGE_USAGE_TRANSFER_DST_BIT), + VmaAllocationCreateInfo({0, VMA_MEMORY_USAGE_GPU_ONLY})); + + AllocatedImage offimgMS( + this, vkh::ImageCreateInfo(4, 4, 0, VK_FORMAT_R16G16B16A16_SFLOAT, + VK_IMAGE_USAGE_TRANSFER_DST_BIT, 1, 1, VK_SAMPLE_COUNT_4_BIT), + VmaAllocationCreateInfo({0, VMA_MEMORY_USAGE_GPU_ONLY})); + + while(Running()) + { + VkCommandBuffer cmd = GetCommandBuffer(); + + vkBeginCommandBuffer(cmd, vkh::CommandBufferBeginInfo()); + + VkImage swapimg = + StartUsingBackbuffer(cmd, VK_ACCESS_TRANSFER_WRITE_BIT, VK_IMAGE_LAYOUT_GENERAL); + + pushMarker(cmd, "Clear"); + vkCmdClearColorImage(cmd, swapimg, VK_IMAGE_LAYOUT_GENERAL, + vkh::ClearColorValue(0.0f, 0.0f, 0.0f, 1.0f), 1, + vkh::ImageSubresourceRange()); + + vkh::cmdPipelineBarrier( + cmd, { + vkh::ImageMemoryBarrier(0, VK_ACCESS_TRANSFER_WRITE_BIT, VK_IMAGE_LAYOUT_UNDEFINED, + VK_IMAGE_LAYOUT_GENERAL, offimg.image), + }); + + vkCmdClearColorImage(cmd, offimg.image, VK_IMAGE_LAYOUT_GENERAL, + vkh::ClearColorValue(0.0f, 0.0f, 0.0f, 1.0f), 1, + vkh::ImageSubresourceRange()); + + vkh::cmdPipelineBarrier( + cmd, { + vkh::ImageMemoryBarrier(0, VK_ACCESS_TRANSFER_WRITE_BIT, VK_IMAGE_LAYOUT_UNDEFINED, + VK_IMAGE_LAYOUT_GENERAL, offimgMS.image), + }); + + vkCmdClearColorImage(cmd, offimgMS.image, VK_IMAGE_LAYOUT_GENERAL, + vkh::ClearColorValue(0.0f, 0.0f, 0.0f, 1.0f), 1, + vkh::ImageSubresourceRange()); + popMarker(cmd); + + vkCmdBeginRenderPass( + cmd, vkh::RenderPassBeginInfo(mainWindow->rp, mainWindow->GetFB(), mainWindow->scissor), + VK_SUBPASS_CONTENTS_INLINE); + + vkCmdBindPipeline(cmd, VK_PIPELINE_BIND_POINT_GRAPHICS, pipe); + vkCmdSetViewport(cmd, 0, 1, &mainWindow->viewport); + vkCmdSetScissor(cmd, 0, 1, &mainWindow->scissor); + vkh::cmdBindVertexBuffers(cmd, 0, {vb.buffer}, {0}); + pushMarker(cmd, "Red: groups of repeated draws"); + for(int i = 1; i <= NUM_TRIANGLES_RED; i *= 2) + { + vkCmdDraw(cmd, 3 * i, 1, TRIANGLES_RED_INDEX, 0); + } + setMarker(cmd, "End of red"); + popMarker(cmd); + pushMarker(cmd, "Green: 255 (the maximum we can handle) in a single drawcall"); + vkCmdDraw(cmd, 3 * NUM_TRIANGLES_GREEN, 1, 3 * TRIANGLES_GREEN_INDEX, 0); + popMarker(cmd); + // Note: this is indistinguishable from 255 triangles since 255 * 1/255 is 1, + // and 512 * 1/255 > 1 but gets clamped to 1 + pushMarker(cmd, "Blue: 512 (more than the maximum) in a single drawcall"); + vkCmdDraw(cmd, 3 * NUM_TRIANGLES_BLUE, 1, 3 * TRIANGLES_BLUE_INDEX, 0); + popMarker(cmd); + + vkCmdEndRenderPass(cmd); + + pushMarker(cmd, "Clear"); + vkCmdClearColorImage(cmd, swapimg, VK_IMAGE_LAYOUT_GENERAL, + vkh::ClearColorValue(0.0f, 0.0f, 0.0f, 1.0f), 1, + vkh::ImageSubresourceRange()); + + vkh::cmdPipelineBarrier( + cmd, { + vkh::ImageMemoryBarrier(0, VK_ACCESS_TRANSFER_WRITE_BIT, VK_IMAGE_LAYOUT_UNDEFINED, + VK_IMAGE_LAYOUT_GENERAL, offimg.image), + }); + + vkCmdClearColorImage(cmd, offimg.image, VK_IMAGE_LAYOUT_GENERAL, + vkh::ClearColorValue(0.0f, 0.0f, 0.0f, 1.0f), 1, + vkh::ImageSubresourceRange()); + + vkh::cmdPipelineBarrier( + cmd, { + vkh::ImageMemoryBarrier(0, VK_ACCESS_TRANSFER_WRITE_BIT, VK_IMAGE_LAYOUT_UNDEFINED, + VK_IMAGE_LAYOUT_GENERAL, offimgMS.image), + }); + + vkCmdClearColorImage(cmd, offimgMS.image, VK_IMAGE_LAYOUT_GENERAL, + vkh::ClearColorValue(0.0f, 0.0f, 0.0f, 1.0f), 1, + vkh::ImageSubresourceRange()); + popMarker(cmd); + + vkCmdBeginRenderPass( + cmd, vkh::RenderPassBeginInfo(mainWindow->rp, mainWindow->GetFB(), mainWindow->scissor), + VK_SUBPASS_CONTENTS_INLINE); + + pushMarker(cmd, "All of the above in a single drawcall"); + // This _is_ distinguishable from 512 blue triangles since we have a mix of red, green, and + // blue + vkCmdDraw(cmd, 3 * NUM_TRIANGLES_TOTAL, 1, 3 * TRIANGLES_RED_INDEX, 0); + popMarker(cmd); + + setMarker(cmd, "Test End"); + + vkCmdEndRenderPass(cmd); + + FinishUsingBackbuffer(cmd, VK_ACCESS_TRANSFER_WRITE_BIT, VK_IMAGE_LAYOUT_GENERAL); + + vkEndCommandBuffer(cmd); + + Submit(0, 1, {cmd}); + + Present(); + } + + return 0; + } +}; + +REGISTER_TEST(); diff --git a/util/test/demos/vk/vk_dual_source.cpp b/util/test/demos/vk/vk_dual_source.cpp new file mode 100644 index 0000000000..ee5116032f --- /dev/null +++ b/util/test/demos/vk/vk_dual_source.cpp @@ -0,0 +1,266 @@ +/****************************************************************************** + * The MIT License (MIT) + * + * Copyright (c) 2019-2023 Baldur Karlsson + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + ******************************************************************************/ + +#include "vk_test.h" + +static std::string common = R"EOSHADER( + +#version 460 core + +#define v2f v2f_block \ +{ \ + vec4 pos; \ + vec4 col; \ + vec4 uv; \ +} + +)EOSHADER"; + +const std::string pixel = common + R"EOSHADER( + +layout(location = 0) in v2f vertIn; + +layout(location = 0, index = 0) out vec4 outColor0; +layout(location = 0, index = 1) out vec4 outColor1; + +void main() { + vec3 fragColor = vertIn.col.rgb; + + outColor0 = vec4(fragColor, 1.0); + outColor1 = vec4(fragColor.brg, 1.0); +} +)EOSHADER"; + +// This shader is equivalent to the other one, just with a much more complicated-looking layout +const std::string pixel_complicated = common + R"EOSHADER( + +layout(location = 0) in v2f vertIn; + +layout(location = 0, index = 0, component = 0) out float outColor0_r; +layout(location = 0, /* index = 0, */ component = 1) out float outColor0_g; +layout(location = 0, /* index = 0, */ component = 2) out vec2 outColor0_ba; +layout(location = 0, index = 1/* , component = 0 */) out float outColor1_r; +layout(location = 0, index = 1, component = 1) out vec2 outColor1_gb; +layout(location = 0, index = 1, component = 3) out float outColor1_a; +// Test so that we have an output builtin. depth_unchanged means nothing interesting +// actually happens (not sure why it exists, but it's nice here) +layout(depth_unchanged) out float gl_FragDepth; + +void main() { + vec4 outColor0, outColor1; + + vec3 fragColor = vertIn.col.rgb; + + outColor0 = vec4(fragColor, 1.0); + outColor1 = vec4(fragColor.brg, 1.0); + + outColor0_r = outColor0.r; + outColor0_g = outColor0.g; + outColor0_ba = outColor0.ba; + outColor1_r = outColor1.r; + outColor1_gb = outColor1.gb; + outColor1_a = outColor1.a; + gl_FragDepth = gl_FragCoord.z; +} +)EOSHADER"; + +RD_TEST(VK_Dual_Source, VulkanGraphicsTest) +{ + static constexpr const char *Description = "Draws a pair of triangles using dual source blending"; + + const DefaultA2V Triangles[12] = { + // Two partially overlapping triangles + {Vec3f(+0.0f, -0.5f, 0.0f), Vec4f(0.5f, 0.0f, 0.0f, 0.0f), Vec2f(0.0f, 0.0f)}, + {Vec3f(+0.5f, +0.5f, 0.0f), Vec4f(0.5f, 0.0f, 0.0f, 0.0f), Vec2f(0.0f, 1.0f)}, + {Vec3f(-0.5f, +0.5f, 0.0f), Vec4f(0.5f, 0.0f, 0.0f, 0.0f), Vec2f(1.0f, 0.0f)}, + {Vec3f(-0.25f, -0.5f, 0.0f), Vec4f(0.5f, 0.0f, 0.5f, 0.0f), Vec2f(0.0f, 0.0f)}, + {Vec3f(+0.75f, -0.5f, 0.0f), Vec4f(0.5f, 0.0f, 0.5f, 0.0f), Vec2f(0.0f, 1.0f)}, + {Vec3f(+0.25f, +0.5f, 0.0f), Vec4f(0.5f, 0.0f, 0.5f, 0.0f), Vec2f(1.0f, 0.0f)}, + }; + + void Prepare(int argc, char **argv) + { + features.dualSrcBlend = VK_TRUE; + VulkanGraphicsTest::Prepare(argc, argv); + } + + void clear(VkCommandBuffer cmd, VkImage swapimg, const AllocatedImage &offimg, + const AllocatedImage &offimgMS, const VkClearColorValue *pColor) + { + pushMarker(cmd, "Clear"); + vkCmdClearColorImage(cmd, swapimg, VK_IMAGE_LAYOUT_GENERAL, pColor, 1, + vkh::ImageSubresourceRange()); + + vkh::cmdPipelineBarrier( + cmd, { + vkh::ImageMemoryBarrier(0, VK_ACCESS_TRANSFER_WRITE_BIT, VK_IMAGE_LAYOUT_UNDEFINED, + VK_IMAGE_LAYOUT_GENERAL, offimg.image), + }); + + vkCmdClearColorImage(cmd, offimg.image, VK_IMAGE_LAYOUT_GENERAL, pColor, 1, + vkh::ImageSubresourceRange()); + + vkh::cmdPipelineBarrier( + cmd, { + vkh::ImageMemoryBarrier(0, VK_ACCESS_TRANSFER_WRITE_BIT, VK_IMAGE_LAYOUT_UNDEFINED, + VK_IMAGE_LAYOUT_GENERAL, offimgMS.image), + }); + + vkCmdClearColorImage(cmd, offimgMS.image, VK_IMAGE_LAYOUT_GENERAL, pColor, 1, + vkh::ImageSubresourceRange()); + popMarker(cmd); + } + + int main() + { + // initialise, create window, create context, etc + requestedSwapChainFormat = VK_FORMAT_B8G8R8A8_UNORM; + if(!Init()) + return 3; + + VkPipelineLayout layout = createPipelineLayout(vkh::PipelineLayoutCreateInfo()); + + vkh::GraphicsPipelineCreateInfo pipeCreateInfo; + + pipeCreateInfo.layout = layout; + pipeCreateInfo.renderPass = mainWindow->rp; + + VkPipelineColorBlendAttachmentState colorBlendAttachment{}; + colorBlendAttachment.colorWriteMask = VK_COLOR_COMPONENT_R_BIT | VK_COLOR_COMPONENT_G_BIT | + VK_COLOR_COMPONENT_B_BIT | VK_COLOR_COMPONENT_A_BIT; + colorBlendAttachment.blendEnable = VK_TRUE; + colorBlendAttachment.srcColorBlendFactor = VK_BLEND_FACTOR_ONE; + colorBlendAttachment.dstColorBlendFactor = VK_BLEND_FACTOR_SRC1_COLOR; + colorBlendAttachment.colorBlendOp = VK_BLEND_OP_ADD; + colorBlendAttachment.srcAlphaBlendFactor = VK_BLEND_FACTOR_ONE; + colorBlendAttachment.dstAlphaBlendFactor = VK_BLEND_FACTOR_ZERO; + colorBlendAttachment.alphaBlendOp = VK_BLEND_OP_ADD; + pipeCreateInfo.colorBlendState.attachments = {colorBlendAttachment}; + + pipeCreateInfo.vertexInputState.vertexBindingDescriptions = {vkh::vertexBind(0, DefaultA2V)}; + pipeCreateInfo.vertexInputState.vertexAttributeDescriptions = { + vkh::vertexAttr(0, 0, DefaultA2V, pos), vkh::vertexAttr(1, 0, DefaultA2V, col), + vkh::vertexAttr(2, 0, DefaultA2V, uv), + }; + + pipeCreateInfo.stages = { + CompileShaderModule(VKDefaultVertex, ShaderLang::glsl, ShaderStage::vert, "main"), + CompileShaderModule(pixel, ShaderLang::glsl, ShaderStage::frag, "main"), + }; + + VkPipeline pipe = createGraphicsPipeline(pipeCreateInfo); + + pipeCreateInfo.stages[1] = + CompileShaderModule(pixel_complicated, ShaderLang::glsl, ShaderStage::frag, "main"); + + VkPipeline complicatedPipe = createGraphicsPipeline(pipeCreateInfo); + + AllocatedBuffer vb( + this, vkh::BufferCreateInfo(sizeof(Triangles), VK_BUFFER_USAGE_VERTEX_BUFFER_BIT | + VK_BUFFER_USAGE_TRANSFER_DST_BIT), + VmaAllocationCreateInfo({0, VMA_MEMORY_USAGE_CPU_TO_GPU})); + + vb.upload(Triangles); + + AllocatedImage offimg(this, vkh::ImageCreateInfo(4, 4, 0, VK_FORMAT_R32G32B32A32_SFLOAT, + VK_IMAGE_USAGE_TRANSFER_DST_BIT), + VmaAllocationCreateInfo({0, VMA_MEMORY_USAGE_GPU_ONLY})); + + AllocatedImage offimgMS( + this, vkh::ImageCreateInfo(4, 4, 0, VK_FORMAT_R16G16B16A16_SFLOAT, + VK_IMAGE_USAGE_TRANSFER_DST_BIT, 1, 1, VK_SAMPLE_COUNT_4_BIT), + VmaAllocationCreateInfo({0, VMA_MEMORY_USAGE_GPU_ONLY})); + + while(Running()) + { + VkCommandBuffer cmd = GetCommandBuffer(); + + vkBeginCommandBuffer(cmd, vkh::CommandBufferBeginInfo()); + + VkImage swapimg = + StartUsingBackbuffer(cmd, VK_ACCESS_TRANSFER_WRITE_BIT, VK_IMAGE_LAYOUT_GENERAL); + + clear(cmd, swapimg, offimg, offimgMS, vkh::ClearColorValue(0.0f, 1.0f, 1.0f, 1.0f)); + + vkCmdBeginRenderPass( + cmd, vkh::RenderPassBeginInfo(mainWindow->rp, mainWindow->GetFB(), mainWindow->scissor), + VK_SUBPASS_CONTENTS_INLINE); + vkCmdBindPipeline(cmd, VK_PIPELINE_BIND_POINT_GRAPHICS, pipe); + vkCmdSetViewport(cmd, 0, 1, &mainWindow->viewport); + vkCmdSetScissor(cmd, 0, 1, &mainWindow->scissor); + vkh::cmdBindVertexBuffers(cmd, 0, {vb.buffer}, {0}); + + setMarker(cmd, "Begin test A1"); + vkCmdDraw(cmd, 6, 1, 0, 0); + setMarker(cmd, "End test A1"); + + vkCmdEndRenderPass(cmd); + + clear(cmd, swapimg, offimg, offimgMS, vkh::ClearColorValue(0.0f, 1.0f, 1.0f, 1.0f)); + + vkCmdBeginRenderPass( + cmd, vkh::RenderPassBeginInfo(mainWindow->rp, mainWindow->GetFB(), mainWindow->scissor), + VK_SUBPASS_CONTENTS_INLINE); + setMarker(cmd, "Begin test A2"); + vkCmdDraw(cmd, 3, 1, 0, 0); + vkCmdDraw(cmd, 3, 1, 3, 0); + setMarker(cmd, "End test A2"); + vkCmdEndRenderPass(cmd); + + clear(cmd, swapimg, offimg, offimgMS, vkh::ClearColorValue(0.0f, 1.0f, 1.0f, 1.0f)); + + vkCmdBeginRenderPass( + cmd, vkh::RenderPassBeginInfo(mainWindow->rp, mainWindow->GetFB(), mainWindow->scissor), + VK_SUBPASS_CONTENTS_INLINE); + vkCmdBindPipeline(cmd, VK_PIPELINE_BIND_POINT_GRAPHICS, complicatedPipe); + setMarker(cmd, "Begin test B1"); + vkCmdDraw(cmd, 6, 1, 0, 0); + setMarker(cmd, "End test B1"); + vkCmdEndRenderPass(cmd); + + clear(cmd, swapimg, offimg, offimgMS, vkh::ClearColorValue(0.0f, 1.0f, 1.0f, 1.0f)); + + vkCmdBeginRenderPass( + cmd, vkh::RenderPassBeginInfo(mainWindow->rp, mainWindow->GetFB(), mainWindow->scissor), + VK_SUBPASS_CONTENTS_INLINE); + setMarker(cmd, "Begin test B2"); + vkCmdDraw(cmd, 3, 1, 0, 0); + vkCmdDraw(cmd, 3, 1, 3, 0); + setMarker(cmd, "End test B2"); + vkCmdEndRenderPass(cmd); + + FinishUsingBackbuffer(cmd, VK_ACCESS_TRANSFER_WRITE_BIT, VK_IMAGE_LAYOUT_GENERAL); + + vkEndCommandBuffer(cmd); + + Submit(0, 1, {cmd}); + + Present(); + } + + return 0; + } +}; + +REGISTER_TEST(); diff --git a/util/test/demos/vk/vk_test.cpp b/util/test/demos/vk/vk_test.cpp index 1ed14b92cc..2267f43cea 100644 --- a/util/test/demos/vk/vk_test.cpp +++ b/util/test/demos/vk/vk_test.cpp @@ -706,7 +706,7 @@ bool VulkanGraphicsTest::Init() if(!headless) { - mainWindow = MakeWindow(screenWidth, screenHeight, "Autotesting"); + mainWindow = MakeWindow(screenWidth, screenHeight, "Autotesting", requestedSwapChainFormat); if(!mainWindow->Initialised()) { @@ -784,7 +784,8 @@ bool VulkanGraphicsTest::Init() return true; } -VulkanWindow *VulkanGraphicsTest::MakeWindow(int width, int height, const char *title) +VulkanWindow *VulkanGraphicsTest::MakeWindow(int width, int height, const char *title, + VkFormat requestedFormat) { #if defined(WIN32) GraphicsWindow *platWin = new Win32Window(width, height, title); @@ -798,7 +799,7 @@ VulkanWindow *VulkanGraphicsTest::MakeWindow(int width, int height, const char * #error UNKNOWN PLATFORM #endif - return new VulkanWindow(this, platWin); + return new VulkanWindow(this, platWin, requestedFormat); } void VulkanGraphicsTest::Shutdown() @@ -1350,11 +1351,12 @@ void VulkanCommands::Submit(const std::vector &cmds, pendingCommandBuffers[1].push_back(std::make_pair(cmd, fence)); } -VulkanWindow::VulkanWindow(VulkanGraphicsTest *test, GraphicsWindow *win) +VulkanWindow::VulkanWindow(VulkanGraphicsTest *test, GraphicsWindow *win, VkFormat requestedFormat) : GraphicsWindow(win->title), VulkanCommands(test) { m_Test = test; m_Win = win; + m_requestedFormat = requestedFormat; { std::lock_guard lock(m_Test->mutex); @@ -1464,7 +1466,7 @@ bool VulkanWindow::CreateSwapchain() for(const VkSurfaceFormatKHR &f : formats) { - if(f.format == VK_FORMAT_B8G8R8A8_SRGB && f.colorSpace == VK_COLOR_SPACE_SRGB_NONLINEAR_KHR) + if(f.format == m_requestedFormat && f.colorSpace == VK_COLOR_SPACE_SRGB_NONLINEAR_KHR) { surfaceFormat = f; break; @@ -1478,6 +1480,10 @@ bool VulkanWindow::CreateSwapchain() } format = surfaceFormat.format; + if(format != m_requestedFormat) + { + TEST_WARN("Wanted surface format %d but got %d", m_requestedFormat, format); + } std::vector modes; CHECK_VKR(vkh::getSurfacePresentModesKHR(modes, m_Test->phys, surface)); diff --git a/util/test/demos/vk/vk_test.h b/util/test/demos/vk/vk_test.h index e5fbc815b1..83271de777 100644 --- a/util/test/demos/vk/vk_test.h +++ b/util/test/demos/vk/vk_test.h @@ -129,13 +129,14 @@ struct VulkanCommands struct VulkanWindow : public GraphicsWindow, public VulkanCommands { + VkFormat m_requestedFormat; VkFormat format; uint32_t imgIndex = 0; VkRenderPass rp = VK_NULL_HANDLE; VkViewport viewport; VkRect2D scissor; - VulkanWindow(VulkanGraphicsTest *test, GraphicsWindow *win); + VulkanWindow(VulkanGraphicsTest *test, GraphicsWindow *win, VkFormat requestedFormat); virtual ~VulkanWindow(); void Shutdown(); @@ -197,7 +198,8 @@ struct VulkanGraphicsTest : public GraphicsTest void Prepare(int argc, char **argv); bool Init(); void Shutdown(); - VulkanWindow *MakeWindow(int width, int height, const char *title); + VulkanWindow *MakeWindow(int width, int height, const char *title, + VkFormat requestedFormat = VK_FORMAT_B8G8R8A8_SRGB); bool Running(); VkImage StartUsingBackbuffer(VkCommandBuffer cmd, @@ -288,6 +290,8 @@ struct VulkanGraphicsTest : public GraphicsTest uint32_t computeQueueFamilyIndex = ~0U; uint32_t transferQueueFamilyIndex = ~0U; + VkFormat requestedSwapChainFormat = VK_FORMAT_B8G8R8A8_SRGB; + bool hasExt(const char *ext); // a custom struct to pass to vkDeviceCreateInfo::pNext diff --git a/util/test/tests/Vulkan/VK_Blend_Pixel_History.py b/util/test/tests/Vulkan/VK_Blend_Pixel_History.py new file mode 100644 index 0000000000..55b0cf260f --- /dev/null +++ b/util/test/tests/Vulkan/VK_Blend_Pixel_History.py @@ -0,0 +1,208 @@ +import renderdoc as rd +import rdtest +from typing import List + +def value_selector(x): return x.floatValue +def passed(x): return x.Passed() +def event_id(x): return x.eventId +def culled(x): return x.backfaceCulled +def depth_test_failed(x): return x.depthTestFailed +def depth_clipped(x): return x.depthClipped +def depth_bounds_failed(x): return x.depthBoundsFailed +def scissor_clipped(x): return x.scissorClipped +def stencil_test_failed(x): return x.stencilTestFailed +def shader_discarded(x): return x.shaderDiscarded +def shader_out_col(x): return value_selector(x.shaderOut.col) +def shader_out_depth(x): return x.shaderOut.depth +def pre_mod_col(x): return value_selector(x.preMod.col) +def post_mod_col(x): return value_selector(x.postMod.col) +def shader_out_depth(x): return x.shaderOut.depth +def pre_mod_depth(x): return x.preMod.depth +def post_mod_depth(x): return x.postMod.depth +def primitive_id(x): return x.primitiveID +def unboundPS(x): return x.unboundPS + +NUM_TRIANGLES_RED = 16 +NUM_TRIANGLES_RED_REAL = NUM_TRIANGLES_RED * 2 - 1 +NUM_TRIANGLES_GREEN = 255 +NUM_TRIANGLES_BLUE = 512 + +class VK_Blend_Pixel_History(rdtest.TestCase): + demos_test_name = 'VK_Blend' + demos_frame_cap = 5 + + def check_capture(self): + apiprops: rd.APIProperties = self.controller.GetAPIProperties() + + if not apiprops.pixelHistory: + rdtest.log.print("Vulkan pixel history not tested") + return + + self.primary_test() + + def primary_test(self): + test_marker: rd.ActionDescription = self.find_action("Test End") + self.controller.SetFrameEvent(test_marker.eventId, True) + + pipe: rd.PipeState = self.controller.GetPipelineState() + + rt: rd.BoundResource = pipe.GetOutputTargets()[0] + + tex = rt.resourceId + tex_details = self.get_texture(tex) + + sub = rd.Subresource() + if tex_details.arraysize > 1: + sub.slice = rt.firstSlice + if tex_details.mips > 1: + sub.mip = rt.firstMip + + red_eid = self.find_action("Red: groups of repeated draws").next.eventId + red_last_eid = self.find_action("End of red").next.eventId + green_eid = self.find_action("Green: 255 (the maximum we can handle) in a single drawcall").next.eventId + blue_eid = self.find_action("Blue: 512 (more than the maximum) in a single drawcall").next.eventId + all_eid = self.find_action("All of the above in a single drawcall").next.eventId + + # Pixel inside of all of the triangles + x, y = 200, 150 + rdtest.log.print("Testing pixel {}, {}".format(x, y)) + modifs: List[rd.PixelModification] = self.controller.PixelHistory(tex, x, y, sub, rt.typeCast) + self.check_modifs_consistent(modifs) + red_modifs = [m for m in modifs if m.eventId >= red_eid and m.eventId < red_last_eid] + green_modifs = [m for m in modifs if m.eventId == green_eid] + blue_modifs = [m for m in modifs if m.eventId == blue_eid] + all_modifs = [m for m in modifs if m.eventId == all_eid] + + if len(red_modifs) != NUM_TRIANGLES_RED_REAL: + raise rdtest.TestFailureException("Expected {} modifications for red triangles (EIDS {} until {}) but got {}".format(NUM_TRIANGLES_RED_REAL, red_eid, red_last_eid, len(red_modifs))) + + for i, modif in enumerate(red_modifs): + if not rdtest.value_compare(modif.shaderOut.col.floatValue, (1.0/255.0, 0.0, 0.0, 1.0), eps=1.0/256.0): + raise rdtest.TestFailureException("Wrong shader output for red triangle {}; got {}, wanted {}".format(i, modif.shaderOut.col.floatValue, (1.0/255.0, 0.0, 0.0, 1.0))) + if not rdtest.value_compare(modif.postMod.col.floatValue, ((i+1)/255.0, 0.0, 0.0, 1.0), eps=1.0/256.0): + raise rdtest.TestFailureException("Wrong post mod for red triangle {}; got {}, wanted {}".format(i, modif.postMod.col.floatValue, ((i+1)/255.0, 0.0, 0.0, 1.0))) + + i = 1 + eid_counter = 0 + modif_counter = 0 + while i <= NUM_TRIANGLES_RED: + for primitive_id in range(i): + if red_modifs[modif_counter].eventId != red_eid + eid_counter: + raise rdtest.TestFailureException("Expected red triangle {} to be part of EID {} but was {}".format(modif_counter, red_eid + eid_counter, red_modifs[modif_counter].eventId)) + if red_modifs[modif_counter].primitiveID != primitive_id: + raise rdtest.TestFailureException("Expected red triangle {} to have primitive ID {} but was {}".format(modif_counter, primitive_id, red_modifs[modif_counter].primitiveID)) + modif_counter += 1 + eid_counter += 1 + i *= 2 + + if len(green_modifs) != NUM_TRIANGLES_GREEN: + raise rdtest.TestFailureException("Expected {} modifications for green triangles (EID {}) but got {}".format(NUM_TRIANGLES_GREEN, green_eid, len(gren_modifs))) + + for i, modif in enumerate(green_modifs): + if modif.primitiveID != i: + raise rdtest.TestFailureException("Expected green triangle {} to have primitive ID {} but was {}".format(i, primitive_id, modif.primitiveID)) + if not rdtest.value_compare(modif.shaderOut.col.floatValue, (0.0, 1.0/255.0, 0.0, 1.0), eps=1.0/256.0): + raise rdtest.TestFailureException("Wrong shader output for green triangle {}; got {}, wanted {}".format(i, modif.shaderOut.col.floatValue, (0.0, 1.0/255.0, 0.0, 1.0))) + if not rdtest.value_compare(modif.postMod.col.floatValue, (NUM_TRIANGLES_RED_REAL/255.0, (i+1)/255.0, 0.0, 1.0), eps=1.0/256.0): + raise rdtest.TestFailureException("Wrong post mod for green triangle {}; got {}, wanted {}".format(i, modif.postMod.col.floatValue, (NUM_TRIANGLES_RED_REAL/255.0, (i+1)/255.0, 0.0, 1.0))) + + # We can only record 255 modifications due to the stencil format + if len(blue_modifs) != 255: + raise rdtest.TestFailureException("Expected {} modifications for blue triangles (EID {}) but got {}".format(255, blue_eid, len(blue_modifs))) + + for i, modif in enumerate(blue_modifs): + if modif.primitiveID != i: + raise rdtest.TestFailureException("Expected blue triangle {} to have primitive ID {} but was {}".format(i, primitive_id, modif.primitiveID)) + if not rdtest.value_compare(modif.shaderOut.col.floatValue, (0.0, 0.0, 1.0/255.0, 1.0), eps=1.0/256.0): + raise rdtest.TestFailureException("Wrong shader output for blue triangle {}; got {}, wanted {}".format(i, modif.shaderOut.col.floatValue, (0.0, 0.0, 1.0/255.0, 1.0))) + if not rdtest.value_compare(modif.postMod.col.floatValue, (NUM_TRIANGLES_RED_REAL/255.0, 1.0, (i+1)/255.0, 1.0), eps=1.0/256.0): + raise rdtest.TestFailureException("Wrong post mod for blue triangle {}; got {}, wanted {}".format(i, modif.postMod.col.floatValue, (NUM_TRIANGLES_RED_REAL/255.0, 1.0, (i+1)/255.0, 1.0))) + + # Once again, we can only record 255 modifications due to the stencil format + if len(all_modifs) != 255: + raise rdtest.TestFailureException("Expected {} modifications for all triangles (EID {}) but got {}".format(255, all_eid, len(all_modifs))) + + for i, modif in enumerate(all_modifs): + if modif.primitiveID != i: + raise rdtest.TestFailureException("Expected triangle {} in all to have primitive ID {} but was {}".format(i, primitive_id, modif.primitiveID)) + + if i < NUM_TRIANGLES_RED: + if not rdtest.value_compare(modif.shaderOut.col.floatValue, (1.0/255.0, 0.0, 0.0, 1.0), eps=1.0/256.0): + raise rdtest.TestFailureException("Wrong shader output for red triangle in all {}; got {}, wanted {}".format(i, modif.shaderOut.col.floatValue, (1.0/255.0, 0.0, 0.0, 1.0))) + if not rdtest.value_compare(modif.postMod.col.floatValue, ((i+1)/255.0, 0.0, 0.0, 1.0), eps=1.0/256.0): + raise rdtest.TestFailureException("Wrong post mod for red triangle in all {}; got {}, wanted {}".format(i, modif.postMod.col.floatValue, ((i+1)/255.0, 0.0, 0.0, 1.0))) + else: + if not rdtest.value_compare(modif.shaderOut.col.floatValue, (0.0, 1.0/255.0, 0.0, 1.0), eps=1.0/256.0): + raise rdtest.TestFailureException("Wrong shader output for green triangle in all {}; got {}, wanted {}".format(i, modif.shaderOut.col.floatValue, (0.0, 1.0/255.0, 0.0, 1.0))) + if i != 254: + if not rdtest.value_compare(modif.postMod.col.floatValue, (NUM_TRIANGLES_RED/255.0, (i+1-NUM_TRIANGLES_RED)/255.0, 0.0, 1.0), eps=1.0/256.0): + raise rdtest.TestFailureException("Wrong post mod for green triangle in all {}; got {}, wanted {}".format(i, modif.postMod.col.floatValue, (NUM_TRIANGLES_RED/255.0, (i+1-NUM_TRIANGLES_RED)/255.0, 0.0, 1.0))) + else: + # For i = 254 (the last triangle), the post-mod value is always set to the final post-mod value, but everything else is correctly set to the 255th modification + if not rdtest.value_compare(modif.postMod.col.floatValue, (NUM_TRIANGLES_RED/255.0, 1.0, 1.0, 1.0), eps=1.0/256.0): + raise rdtest.TestFailureException("Wrong post mod for final (blue) triangle in all {}; got {}, wanted {}".format(i, modif.postMod.col.floatValue, (NUM_TRIANGLES_RED/255.0, 1.0, 1.0, 1.0))) + + def check_modifs_consistent(self, modifs): + # postmod of each should match premod of the next + for i in range(len(modifs) - 1): + a = value_selector(modifs[i].postMod.col) + b = value_selector(modifs[i + 1].preMod.col) + + if a != b: + raise rdtest.TestFailureException( + "postmod at {} primitive {}: {} doesn't match premod at {} primitive {}: {}".format(modifs[i].eventId, + modifs[i].primitiveID, + a, + modifs[i + 1].eventId, + modifs[i + 1].primitiveID, + b)) + + + # With this test's blend configuration, shaderOut and blendSrc should match + for i in range(len(modifs)): + if not modifs[i].blendSrc.IsValid(): # Applies to clears + continue + + a = value_selector(modifs[i].shaderOut.col) + b = value_selector(modifs[i].blendSrc.col) + + if not rdtest.value_compare(a, b, eps=1.0/256.0): + raise rdtest.TestFailureException( + "shaderOut at {} primitive {}: {} doesn't match blendSrc: {}".format(modifs[i].eventId, + modifs[i].primitiveID, + a, b)) + + # With this test's blend configuration, preMod and blendDst should match + for i in range(len(modifs)): + if not modifs[i].blendDst.IsValid(): # Applies to clears + continue + + a = value_selector(modifs[i].preMod.col)[:3] + b = value_selector(modifs[i].blendDst.col)[:3] + + if not rdtest.value_compare(a, b, eps=1.0/256.0): + raise rdtest.TestFailureException( + "preMod at {} primitive {}: {} doesn't match blendDst: {}".format(modifs[i].eventId, + modifs[i].primitiveID, + a, b)) + + # With this test's blend configuration, and good driver behavior, blendSrc + blendDst should be about equal to postMod + for i in range(len(modifs)): + if not modifs[i].blendDst.IsValid(): # Applies to clears + continue + + if i == len(modifs) - 1: + # Final modifs entry, doesn't behave nicely because postMod is wrong + # This doesn't affect the blue trianges since everything after 255 has blue of 1.0 + continue + + a = value_selector(modifs[i].blendSrc.col) + b = value_selector(modifs[i].blendDst.col) + sum_a_b = [sum(x) for x in zip(a, b)] + c = value_selector(modifs[i].postMod.col) + + if not rdtest.value_compare(sum_a_b, c, eps=1.0/256.0): + raise rdtest.TestFailureException( + "blendSrc at {} primitive {}: {} plus blendDst: {} (sum: {}) should equal postMod: {}".format(modifs[i].eventId, + modifs[i].primitiveID, + a, b, sum_a_b, c)) diff --git a/util/test/tests/Vulkan/VK_Dual_Source.py b/util/test/tests/Vulkan/VK_Dual_Source.py new file mode 100644 index 0000000000..2b3fce03bb --- /dev/null +++ b/util/test/tests/Vulkan/VK_Dual_Source.py @@ -0,0 +1,128 @@ +import renderdoc as rd +import rdtest +from typing import List + +def value_selector(x): return x.floatValue +def passed(x): return x.Passed() +def event_id(x): return x.eventId +def culled(x): return x.backfaceCulled +def depth_test_failed(x): return x.depthTestFailed +def depth_clipped(x): return x.depthClipped +def depth_bounds_failed(x): return x.depthBoundsFailed +def scissor_clipped(x): return x.scissorClipped +def stencil_test_failed(x): return x.stencilTestFailed +def shader_discarded(x): return x.shaderDiscarded +def shader_out_col(x): return value_selector(x.shaderOut.col) +def shader_out_depth(x): return x.shaderOut.depth +def pre_mod_col(x): return value_selector(x.preMod.col) +def post_mod_col(x): return value_selector(x.postMod.col) +def shader_out_depth(x): return x.shaderOut.depth +def pre_mod_depth(x): return x.preMod.depth +def post_mod_depth(x): return x.postMod.depth +def primitive_id(x): return x.primitiveID +def unboundPS(x): return x.unboundPS + +NUM_TRIANGLES_RED = 16 +NUM_TRIANGLES_RED_REAL = NUM_TRIANGLES_RED * 2 - 1 +NUM_TRIANGLES_GREEN = 255 +NUM_TRIANGLES_BLUE = 512 + +class VK_Dual_Source(rdtest.TestCase): + demos_test_name = 'VK_Dual_Source' + demos_frame_cap = 5 + + def check_capture(self): + apiprops: rd.APIProperties = self.controller.GetAPIProperties() + + if not apiprops.pixelHistory: + rdtest.log.print("Vulkan pixel history not tested") + return + + self.primary_test() + + def primary_test(self): + test_marker: rd.ActionDescription = self.find_action("End test B2") + self.controller.SetFrameEvent(test_marker.eventId, True) + + pipe: rd.PipeState = self.controller.GetPipelineState() + + rt: rd.BoundResource = pipe.GetOutputTargets()[0] + + tex = rt.resourceId + tex_details = self.get_texture(tex) + + sub = rd.Subresource() + if tex_details.arraysize > 1: + sub.slice = rt.firstSlice + if tex_details.mips > 1: + sub.mip = rt.firstMip + + # Pixel inside first triangle + modifs_1: List[rd.PixelModification] = self.controller.PixelHistory(tex, 175, 125, sub, rt.typeCast) + # Pixel inside second triangle + modifs_2: List[rd.PixelModification] = self.controller.PixelHistory(tex, 275, 175, sub, rt.typeCast) + # Pixel inside both triangles + modifs_both: List[rd.PixelModification] = self.controller.PixelHistory(tex, 225, 150, sub, rt.typeCast) + + self.check_range(modifs_1, modifs_2, modifs_both, "A1") + self.check_range(modifs_1, modifs_2, modifs_both, "A2") + self.check_range(modifs_1, modifs_2, modifs_both, "B1") + self.check_range(modifs_1, modifs_2, modifs_both, "B2") + + def check_range(self, orig_modifs_1, orig_modifs_2, orig_modifs_both, group): + start_eid = self.find_action("Begin test " + group).eventId + end_eid = self.find_action("End test " + group).eventId + modifs_1 = [m for m in orig_modifs_1 if m.eventId > start_eid and m.eventId < end_eid] + modifs_2 = [m for m in orig_modifs_2 if m.eventId > start_eid and m.eventId < end_eid] + modifs_both = [m for m in orig_modifs_both if m.eventId > start_eid and m.eventId < end_eid] + if not len(modifs_1) == 1: + raise rdtest.TestFailureException( + "group {}: expected 1 modification but found {}".format(group, len(modifs_1))) + if not len(modifs_2) == 1: + raise rdtest.TestFailureException( + "group {}: expected 1 modification but found {}".format(group, len(modifs_2))) + if not len(modifs_both) == 2: + raise rdtest.TestFailureException( + "group {}: expected 2 modifications but found {}".format(group, len(modifs_both))) + modif_1 = modifs_1[0] + modif_2 = modifs_2[0] + modif_both_1 = modifs_both[0] + modif_both_2 = modifs_both[1] + # modif_both_1 should match modif_1 as both are the same triangle on the same background + assert(value_selector(modif_1.preMod.col) == value_selector(modif_both_1.preMod.col)) + assert(value_selector(modif_1.shaderOut.col) == value_selector(modif_both_1.shaderOut.col)) + assert(value_selector(modif_1.shaderOutDualSrc.col) == value_selector(modif_both_1.shaderOutDualSrc.col)) + assert(value_selector(modif_1.blendSrc.col) == value_selector(modif_both_1.blendSrc.col)) + assert(value_selector(modif_1.blendDst.col) == value_selector(modif_both_1.blendDst.col)) + assert(value_selector(modif_1.postMod.col) == value_selector(modif_both_1.postMod.col)) + + # Triangle 1 outputs (0.5, 0.0, 0.0, 1.0) to source 0, (0.0, 0.5, 0.0, 1.0) to source 1 + # Background is (0.0, 1.0, 1.0, 1.0) + # Blend is source0*1 + dest*source1, so the final output should be (0.5, 0.5, 0.0, 1.0) + # eps=.01 because different GPUs handle blending differently + epsilon = .01 + assert(rdtest.value_compare(value_selector(modif_1.preMod.col), (0.0, 1.0, 1.0, 1.0), eps=epsilon)) + assert(rdtest.value_compare(value_selector(modif_1.shaderOut.col), (0.5, 0.0, 0.0, 1.0), eps=epsilon)) + assert(rdtest.value_compare(value_selector(modif_1.shaderOutDualSrc.col), (0.0, 0.5, 0.0, 1.0), eps=epsilon)) + assert(rdtest.value_compare(value_selector(modif_1.blendSrc.col), (0.5, 0.0, 0.0, 1.0), eps=epsilon)) + assert(rdtest.value_compare(value_selector(modif_1.blendDst.col), (0.0, 0.5, 0.0, 0.0), eps=epsilon)) + assert(rdtest.value_compare(value_selector(modif_1.postMod.col), (0.5, 0.5, 0.0, 1.0), eps=epsilon)) + + # Triangle 2 outputs (0.5, 0.0, 0.5, 1.0) to source 0, (0.5, 0.5, 0.0, 1.0) to source 1 + # On the (0.0, 1.0, 1.0, 1.0) background the final output is (0.5, 0.0, 0.5, 1.0) added + # to (0.0, 0.5, 0.0, 0.0) = (0.5, 0.5, 0.5, 0.0) + assert(rdtest.value_compare(value_selector(modif_2.preMod.col), (0.0, 1.0, 1.0, 1.0), eps=epsilon)) + assert(rdtest.value_compare(value_selector(modif_2.shaderOut.col), (0.5, 0.0, 0.5, 1.0), eps=epsilon)) + assert(rdtest.value_compare(value_selector(modif_2.shaderOutDualSrc.col), (0.5, 0.5, 0.0, 1.0), eps=epsilon)) + assert(rdtest.value_compare(value_selector(modif_2.blendSrc.col), (0.5, 0.0, 0.5, 1.0), eps=epsilon)) + assert(rdtest.value_compare(value_selector(modif_2.blendDst.col), (0.0, 0.5, 0.0, 0.0), eps=epsilon)) + assert(rdtest.value_compare(value_selector(modif_2.postMod.col), (0.5, 0.5, 0.5, 1.0), eps=epsilon)) + + # Triangle 2 on top of triangle 1's output of (0.5, 0.5, 0.0, 1.0) is (0.5, 0.0, 0.5, 1.0) + # added to (0.25, 0.25, 0.0, 0.0) = (0.75, 0.25, 0.5, 1.0) + assert(rdtest.value_compare(value_selector(modif_both_2.preMod.col), (0.5, 0.5, 0.0, 1.0), eps=epsilon)) + assert(rdtest.value_compare(value_selector(modif_both_2.shaderOut.col), (0.5, 0.0, 0.5, 1.0), eps=epsilon)) + assert(rdtest.value_compare(value_selector(modif_both_2.shaderOutDualSrc.col), (0.5, 0.5, 0.0, 1.0), eps=epsilon)) + assert(rdtest.value_compare(value_selector(modif_both_2.blendSrc.col), (0.5, 0.0, 0.5, 1.0), eps=epsilon)) + assert(rdtest.value_compare(value_selector(modif_both_2.blendDst.col), (0.25, 0.25, 0.0, 0.0), eps=epsilon)) + assert(rdtest.value_compare(value_selector(modif_both_2.postMod.col), (0.75, 0.25, 0.5, 1.0), eps=epsilon))