Source code

Revision control

Copy as Markdown

Other Tools

/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
#include "WebGLContext.h"
#include "MozFramebuffer.h"
#include "GLContext.h"
#include "mozilla/CheckedInt.h"
#include "mozilla/ProfilerLabels.h"
#include "mozilla/ScopeExit.h"
#include "mozilla/StaticPrefs_webgl.h"
#include "mozilla/UniquePtrExtensions.h"
#include "nsPrintfCString.h"
#include "WebGLBuffer.h"
#include "WebGLContextUtils.h"
#include "WebGLFormats.h"
#include "WebGLFramebuffer.h"
#include "WebGLProgram.h"
#include "WebGLRenderbuffer.h"
#include "WebGLShader.h"
#include "WebGLTexture.h"
#include "WebGLTransformFeedback.h"
#include "WebGLVertexArray.h"
#include <algorithm>
namespace mozilla {
// For a Tegra workaround.
static const int MAX_DRAW_CALLS_SINCE_FLUSH = 100;
////////////////////////////////////////
class ScopedResolveTexturesForDraw {
struct TexRebindRequest {
uint32_t texUnit;
WebGLTexture* tex;
};
WebGLContext* const mWebGL;
std::vector<TexRebindRequest> mRebindRequests;
public:
ScopedResolveTexturesForDraw(WebGLContext* webgl, bool* const out_error);
~ScopedResolveTexturesForDraw();
};
static bool ValidateNoSamplingFeedback(const WebGLTexture& tex,
const uint32_t sampledLevels,
const WebGLFramebuffer* const fb,
const uint32_t texUnit) {
if (!fb) return true;
const auto& texAttachments = fb->GetCompletenessInfo()->texAttachments;
for (const auto& attach : texAttachments) {
if (attach->Texture() != &tex) continue;
const auto& srcBase = tex.Es3_level_base();
const auto srcLast = srcBase + sampledLevels - 1;
const auto& dstLevel = attach->MipLevel();
if (MOZ_UNLIKELY(srcBase <= dstLevel && dstLevel <= srcLast)) {
const auto& webgl = tex.mContext;
const auto& texTargetStr = EnumString(tex.Target().get());
const auto& attachStr = EnumString(attach->mAttachmentPoint);
webgl->ErrorInvalidOperation(
"Texture level %u would be read by %s unit %u,"
" but written by framebuffer attachment %s,"
" which would be illegal feedback.",
dstLevel, texTargetStr.c_str(), texUnit, attachStr.c_str());
return false;
}
}
return true;
}
ScopedResolveTexturesForDraw::ScopedResolveTexturesForDraw(
WebGLContext* webgl, bool* const out_error)
: mWebGL(webgl) {
const auto& fb = mWebGL->mBoundDrawFramebuffer;
struct SamplerByTexUnit {
uint8_t texUnit;
const webgl::SamplerUniformInfo* sampler;
};
Vector<SamplerByTexUnit, 8> samplerByTexUnit;
MOZ_ASSERT(mWebGL->mActiveProgramLinkInfo);
const auto& samplerUniforms = mWebGL->mActiveProgramLinkInfo->samplerUniforms;
for (const auto& pUniform : samplerUniforms) {
const auto& uniform = *pUniform;
const auto& texList = uniform.texListForType;
const auto& uniformBaseType = uniform.texBaseType;
for (const auto& texUnit : uniform.texUnits) {
MOZ_ASSERT(texUnit < texList.Length());
{
decltype(SamplerByTexUnit::sampler) prevSamplerForTexUnit = nullptr;
for (const auto& cur : samplerByTexUnit) {
if (cur.texUnit == texUnit) {
prevSamplerForTexUnit = cur.sampler;
}
}
if (!prevSamplerForTexUnit) {
prevSamplerForTexUnit = &uniform;
MOZ_RELEASE_ASSERT(samplerByTexUnit.append(
SamplerByTexUnit{texUnit, prevSamplerForTexUnit}));
}
if (MOZ_UNLIKELY(&uniform.texListForType !=
&prevSamplerForTexUnit->texListForType)) {
// Pointing to different tex lists means different types!
const auto linkInfo = mWebGL->mActiveProgramLinkInfo;
const auto LocInfoBySampler = [&](const webgl::SamplerUniformInfo* p)
-> const webgl::LocationInfo* {
for (const auto& pair : linkInfo->locationMap) {
const auto& locInfo = pair.second;
if (locInfo.samplerInfo == p) {
return &locInfo;
}
}
MOZ_CRASH("Can't find sampler location.");
};
const auto& cur = *LocInfoBySampler(&uniform);
const auto& prev = *LocInfoBySampler(prevSamplerForTexUnit);
mWebGL->ErrorInvalidOperation(
"Tex unit %u referenced by samplers of different types:"
" %s (via %s) and %s (via %s).",
texUnit, EnumString(cur.info.info.elemType).c_str(),
cur.PrettyName().c_str(),
EnumString(prev.info.info.elemType).c_str(),
prev.PrettyName().c_str());
*out_error = true;
return;
}
}
const auto& tex = texList[texUnit];
if (!tex) continue;
const auto& sampler = mWebGL->mBoundSamplers[texUnit];
const auto& samplingInfo = tex->GetSampleableInfo(sampler.get());
if (MOZ_UNLIKELY(!samplingInfo)) { // There was an error.
*out_error = true;
return;
}
if (MOZ_UNLIKELY(!samplingInfo->IsComplete())) {
if (samplingInfo->incompleteReason) {
const auto& targetName = GetEnumName(tex->Target().get());
mWebGL->GenerateWarning("%s at unit %u is incomplete: %s", targetName,
texUnit, samplingInfo->incompleteReason);
}
mRebindRequests.push_back({texUnit, tex});
continue;
}
// We have more validation to do if we're otherwise complete:
const auto& texBaseType = samplingInfo->usage->format->baseType;
if (MOZ_UNLIKELY(texBaseType != uniformBaseType)) {
const auto& targetName = GetEnumName(tex->Target().get());
const auto& srcType = ToString(texBaseType);
const auto& dstType = ToString(uniformBaseType);
mWebGL->ErrorInvalidOperation(
"%s at unit %u is of type %s, but"
" the shader samples as %s.",
targetName, texUnit, srcType, dstType);
*out_error = true;
return;
}
if (MOZ_UNLIKELY(uniform.isShadowSampler !=
samplingInfo->isDepthTexCompare)) {
const auto& targetName = GetEnumName(tex->Target().get());
mWebGL->ErrorInvalidOperation(
"%s at unit %u is%s a depth texture"
" with TEXTURE_COMPARE_MODE, but"
" the shader sampler is%s a shadow"
" sampler.",
targetName, texUnit, samplingInfo->isDepthTexCompare ? "" : " not",
uniform.isShadowSampler ? "" : " not");
*out_error = true;
return;
}
if (MOZ_UNLIKELY(!ValidateNoSamplingFeedback(*tex, samplingInfo->levels,
fb.get(), texUnit))) {
*out_error = true;
return;
}
}
}
const auto& gl = mWebGL->gl;
for (const auto& itr : mRebindRequests) {
gl->fActiveTexture(LOCAL_GL_TEXTURE0 + itr.texUnit);
GLuint incompleteTex = 0; // Tex 0 is always incomplete.
const auto& overrideTex = webgl->mIncompleteTexOverride;
if (overrideTex) {
// In all but the simplest cases, this will be incomplete anyway, since
// e.g. int-samplers need int-textures. This is useful for e.g.
// dom-to-texture failures, though.
incompleteTex = overrideTex->name;
}
gl->fBindTexture(itr.tex->Target().get(), incompleteTex);
}
}
ScopedResolveTexturesForDraw::~ScopedResolveTexturesForDraw() {
if (mRebindRequests.empty()) return;
gl::GLContext* gl = mWebGL->gl;
for (const auto& itr : mRebindRequests) {
gl->fActiveTexture(LOCAL_GL_TEXTURE0 + itr.texUnit);
gl->fBindTexture(itr.tex->Target().get(), itr.tex->mGLName);
}
gl->fActiveTexture(LOCAL_GL_TEXTURE0 + mWebGL->mActiveTexture);
}
////////////////////////////////////////
bool WebGLContext::ValidateStencilParamsForDrawCall() const {
const auto stencilBits = [&]() -> uint8_t {
if (!mStencilTestEnabled) return 0;
if (!mBoundDrawFramebuffer) return mOptions.stencil ? 8 : 0;
if (mBoundDrawFramebuffer->StencilAttachment().HasAttachment()) return 8;
if (mBoundDrawFramebuffer->DepthStencilAttachment().HasAttachment())
return 8;
return 0;
}();
const uint32_t stencilMax = (1 << stencilBits) - 1;
const auto fnMask = [&](const uint32_t x) { return x & stencilMax; };
const auto fnClamp = [&](const int32_t x) {
return std::max(0, std::min(x, (int32_t)stencilMax));
};
bool ok = true;
ok &= (fnMask(mStencilWriteMaskFront) == fnMask(mStencilWriteMaskBack));
ok &= (fnMask(mStencilValueMaskFront) == fnMask(mStencilValueMaskBack));
ok &= (fnClamp(mStencilRefFront) == fnClamp(mStencilRefBack));
if (!ok) {
ErrorInvalidOperation(
"Stencil front/back state must effectively match."
" (before front/back comparison, WRITEMASK and VALUE_MASK"
" are masked with (2^s)-1, and REF is clamped to"
" [0, (2^s)-1], where `s` is the number of enabled stencil"
" bits in the draw framebuffer)");
}
return ok;
}
// -
void WebGLContext::GenErrorIllegalUse(const GLenum useTarget,
const uint32_t useId,
const GLenum boundTarget,
const uint32_t boundId) const {
const auto fnName = [&](const GLenum target, const uint32_t id) {
auto name = nsCString(EnumString(target).c_str());
if (id != static_cast<uint32_t>(-1)) {
name += nsPrintfCString("[%u]", id);
}
return name;
};
const auto& useName = fnName(useTarget, useId);
const auto& boundName = fnName(boundTarget, boundId);
GenerateError(LOCAL_GL_INVALID_OPERATION,
"Illegal use of buffer at %s"
" while also bound to %s.",
useName.BeginReading(), boundName.BeginReading());
}
bool WebGLContext::ValidateBufferForNonTf(const WebGLBuffer& nonTfBuffer,
const GLenum nonTfTarget,
const uint32_t nonTfId) const {
bool dupe = false;
const auto& tfAttribs = mBoundTransformFeedback->mIndexedBindings;
for (const auto& cur : tfAttribs) {
dupe |= (&nonTfBuffer == cur.mBufferBinding.get());
}
if (MOZ_LIKELY(!dupe)) return true;
dupe = false;
for (const auto tfId : IntegerRange(tfAttribs.size())) {
const auto& tfBuffer = tfAttribs[tfId].mBufferBinding;
if (&nonTfBuffer == tfBuffer) {
dupe = true;
GenErrorIllegalUse(nonTfTarget, nonTfId,
LOCAL_GL_TRANSFORM_FEEDBACK_BUFFER, tfId);
}
}
MOZ_ASSERT(dupe);
return false;
}
bool WebGLContext::ValidateBuffersForTf(
const WebGLTransformFeedback& tfo,
const webgl::LinkedProgramInfo& linkInfo) const {
size_t numUsed;
switch (linkInfo.transformFeedbackBufferMode) {
case LOCAL_GL_INTERLEAVED_ATTRIBS:
numUsed = 1;
break;
case LOCAL_GL_SEPARATE_ATTRIBS:
numUsed = linkInfo.active.activeTfVaryings.size();
break;
default:
MOZ_CRASH();
}
std::vector<webgl::BufferAndIndex> tfBuffers;
tfBuffers.reserve(numUsed);
for (const auto i : IntegerRange(numUsed)) {
tfBuffers.push_back({tfo.mIndexedBindings[i].mBufferBinding.get(),
static_cast<uint32_t>(i)});
}
return ValidateBuffersForTf(tfBuffers);
}
bool WebGLContext::ValidateBuffersForTf(
const std::vector<webgl::BufferAndIndex>& tfBuffers) const {
bool dupe = false;
const auto fnCheck = [&](const WebGLBuffer* const nonTf,
const GLenum nonTfTarget, const uint32_t nonTfId) {
for (const auto& tf : tfBuffers) {
dupe |= (nonTf && tf.buffer == nonTf);
}
if (MOZ_LIKELY(!dupe)) return false;
for (const auto& tf : tfBuffers) {
if (nonTf && tf.buffer == nonTf) {
dupe = true;
GenErrorIllegalUse(LOCAL_GL_TRANSFORM_FEEDBACK_BUFFER, tf.id,
nonTfTarget, nonTfId);
}
}
return true;
};
fnCheck(mBoundArrayBuffer.get(), LOCAL_GL_ARRAY_BUFFER, -1);
fnCheck(mBoundCopyReadBuffer.get(), LOCAL_GL_COPY_READ_BUFFER, -1);
fnCheck(mBoundCopyWriteBuffer.get(), LOCAL_GL_COPY_WRITE_BUFFER, -1);
fnCheck(mBoundPixelPackBuffer.get(), LOCAL_GL_PIXEL_PACK_BUFFER, -1);
fnCheck(mBoundPixelUnpackBuffer.get(), LOCAL_GL_PIXEL_UNPACK_BUFFER, -1);
// fnCheck(mBoundTransformFeedbackBuffer.get(),
// LOCAL_GL_TRANSFORM_FEEDBACK_BUFFER, -1);
fnCheck(mBoundUniformBuffer.get(), LOCAL_GL_UNIFORM_BUFFER, -1);
for (const auto i : IntegerRange(mIndexedUniformBufferBindings.size())) {
const auto& cur = mIndexedUniformBufferBindings[i];
fnCheck(cur.mBufferBinding.get(), LOCAL_GL_UNIFORM_BUFFER, i);
}
fnCheck(mBoundVertexArray->mElementArrayBuffer.get(),
LOCAL_GL_ELEMENT_ARRAY_BUFFER, -1);
for (const auto i : IntegerRange(MaxVertexAttribs())) {
const auto& binding = mBoundVertexArray->AttribBinding(i);
fnCheck(binding.buffer.get(), LOCAL_GL_ARRAY_BUFFER, i);
}
return !dupe;
}
////////////////////////////////////////
template <typename T>
static bool DoSetsIntersect(const std::set<T>& a, const std::set<T>& b) {
std::vector<T> intersection;
std::set_intersection(a.begin(), a.end(), b.begin(), b.end(),
std::back_inserter(intersection));
return !intersection.empty();
}
template <size_t N>
static size_t FindFirstOne(const std::bitset<N>& bs) {
MOZ_ASSERT(bs.any());
// We don't need this to be fast, so don't bother with CLZ intrinsics.
for (const auto i : IntegerRange(N)) {
if (bs[i]) return i;
}
return -1;
}
const webgl::CachedDrawFetchLimits* ValidateDraw(WebGLContext* const webgl,
const GLenum mode,
const uint32_t instanceCount) {
if (!webgl->BindCurFBForDraw()) return nullptr;
const auto& fb = webgl->mBoundDrawFramebuffer;
if (fb) {
const auto& info = *fb->GetCompletenessInfo();
const auto isF32WithBlending = info.isAttachmentF32 & webgl->mBlendEnabled;
if (isF32WithBlending.any()) {
if (!webgl->IsExtensionEnabled(WebGLExtensionID::EXT_float_blend)) {
const auto first = FindFirstOne(isF32WithBlending);
webgl->ErrorInvalidOperation(
"Attachment %u is float32 with blending enabled, which requires "
"EXT_float_blend.",
uint32_t(first));
return nullptr;
}
webgl->WarnIfImplicit(WebGLExtensionID::EXT_float_blend);
}
}
switch (mode) {
case LOCAL_GL_TRIANGLES:
case LOCAL_GL_TRIANGLE_STRIP:
case LOCAL_GL_TRIANGLE_FAN:
case LOCAL_GL_POINTS:
case LOCAL_GL_LINE_STRIP:
case LOCAL_GL_LINE_LOOP:
case LOCAL_GL_LINES:
break;
default:
webgl->ErrorInvalidEnumInfo("mode", mode);
return nullptr;
}
if (!webgl->ValidateStencilParamsForDrawCall()) return nullptr;
if (!webgl->mActiveProgramLinkInfo) {
webgl->ErrorInvalidOperation("The current program is not linked.");
return nullptr;
}
const auto& linkInfo = webgl->mActiveProgramLinkInfo;
// -
// Check UBO sizes.
for (const auto i : IntegerRange(linkInfo->uniformBlocks.size())) {
const auto& cur = linkInfo->uniformBlocks[i];
const auto& dataSize = cur.info.dataSize;
const auto& binding = cur.binding;
if (!binding) {
webgl->ErrorInvalidOperation("Buffer for uniform block is null.");
return nullptr;
}
const auto availByteCount = binding->ByteCount();
if (dataSize > availByteCount) {
webgl->ErrorInvalidOperation(
"Buffer for uniform block is smaller"
" than UNIFORM_BLOCK_DATA_SIZE.");
return nullptr;
}
if (!webgl->ValidateBufferForNonTf(binding->mBufferBinding,
LOCAL_GL_UNIFORM_BUFFER, i))
return nullptr;
}
// -
const auto& tfo = webgl->mBoundTransformFeedback;
if (tfo && tfo->IsActiveAndNotPaused()) {
if (fb) {
const auto& info = *fb->GetCompletenessInfo();
if (info.isMultiview) {
webgl->ErrorInvalidOperation(
"Cannot render to multiview with transform feedback.");
return nullptr;
}
}
if (!webgl->ValidateBuffersForTf(*tfo, *linkInfo)) return nullptr;
}
// -
const auto& fragOutputs = linkInfo->fragOutputs;
const auto fnValidateFragOutputType =
[&](const uint8_t loc, const webgl::TextureBaseType dstBaseType) {
const auto itr = fragOutputs.find(loc);
MOZ_DIAGNOSTIC_ASSERT(itr != fragOutputs.end());
const auto& info = itr->second;
const auto& srcBaseType = info.baseType;
if (MOZ_UNLIKELY(dstBaseType != srcBaseType)) {
const auto& srcStr = ToString(srcBaseType);
const auto& dstStr = ToString(dstBaseType);
webgl->ErrorInvalidOperation(
"Program frag output at location %u is type %s,"
" but destination draw buffer is type %s.",
uint32_t(loc), srcStr, dstStr);
return false;
}
return true;
};
if (!webgl->mRasterizerDiscardEnabled) {
uint8_t fbZLayerCount = 1;
auto hasAttachment = std::bitset<webgl::kMaxDrawBuffers>(1);
auto drawBufferEnabled = std::bitset<webgl::kMaxDrawBuffers>();
if (fb) {
drawBufferEnabled = fb->DrawBufferEnabled();
const auto& info = *fb->GetCompletenessInfo();
fbZLayerCount = info.zLayerCount;
hasAttachment = info.hasAttachment;
} else {
drawBufferEnabled[0] = (webgl->mDefaultFB_DrawBuffer0 == LOCAL_GL_BACK);
}
if (fbZLayerCount != linkInfo->zLayerCount) {
webgl->ErrorInvalidOperation(
"Multiview count mismatch: shader: %u, framebuffer: %u",
uint32_t{linkInfo->zLayerCount}, uint32_t{fbZLayerCount});
return nullptr;
}
const auto writable =
hasAttachment & drawBufferEnabled & webgl->mColorWriteMaskNonzero;
if (writable.any()) {
// Do we have any undefined outputs with real attachments that
// aren't masked-out by color write mask or drawBuffers?
const auto wouldWriteUndefined = ~linkInfo->hasOutput & writable;
if (wouldWriteUndefined.any()) {
const auto first = FindFirstOne(wouldWriteUndefined);
webgl->ErrorInvalidOperation(
"Program has no frag output at location %u, the"
" destination draw buffer has an attached"
" image, and its color write mask is not all false,"
" and DRAW_BUFFER%u is not NONE.",
uint32_t(first), uint32_t(first));
return nullptr;
}
const auto outputWrites = linkInfo->hasOutput & writable;
if (fb) {
for (const auto& attach : fb->ColorDrawBuffers()) {
const auto i =
uint8_t(attach->mAttachmentPoint - LOCAL_GL_COLOR_ATTACHMENT0);
if (!outputWrites[i]) continue;
const auto& imageInfo = attach->GetImageInfo();
if (!imageInfo) continue;
const auto& dstBaseType = imageInfo->mFormat->format->baseType;
if (!fnValidateFragOutputType(i, dstBaseType)) return nullptr;
}
} else {
if (outputWrites[0]) {
if (!fnValidateFragOutputType(0, webgl::TextureBaseType::Float))
return nullptr;
}
}
}
}
// -
const auto fetchLimits = linkInfo->GetDrawFetchLimits();
if (!fetchLimits) return nullptr;
if (instanceCount > fetchLimits->maxInstances) {
webgl->ErrorInvalidOperation(
"Instance fetch requires %u, but attribs only"
" supply %u.",
instanceCount, uint32_t(fetchLimits->maxInstances));
return nullptr;
}
if (tfo) {
for (const auto& used : fetchLimits->usedBuffers) {
MOZ_ASSERT(used.buffer);
if (!webgl->ValidateBufferForNonTf(*used.buffer, LOCAL_GL_ARRAY_BUFFER,
used.id))
return nullptr;
}
}
// -
webgl->RunContextLossTimer();
return fetchLimits;
}
////////////////////////////////////////
static uint32_t UsedVertsForTFDraw(GLenum mode, uint32_t vertCount) {
uint8_t vertsPerPrim;
switch (mode) {
case LOCAL_GL_POINTS:
vertsPerPrim = 1;
break;
case LOCAL_GL_LINES:
vertsPerPrim = 2;
break;
case LOCAL_GL_TRIANGLES:
vertsPerPrim = 3;
break;
default:
MOZ_CRASH("`mode`");
}
return vertCount / vertsPerPrim * vertsPerPrim;
}
class ScopedDrawWithTransformFeedback final {
WebGLContext* const mWebGL;
WebGLTransformFeedback* const mTFO;
const bool mWithTF;
uint32_t mUsedVerts;
public:
ScopedDrawWithTransformFeedback(WebGLContext* webgl, GLenum mode,
uint32_t vertCount, uint32_t instanceCount,
bool* const out_error)
: mWebGL(webgl),
mTFO(mWebGL->mBoundTransformFeedback),
mWithTF(mTFO && mTFO->mIsActive && !mTFO->mIsPaused),
mUsedVerts(0) {
*out_error = false;
if (!mWithTF) return;
if (mode != mTFO->mActive_PrimMode) {
mWebGL->ErrorInvalidOperation(
"Drawing with transform feedback requires"
" `mode` to match BeginTransformFeedback's"
" `primitiveMode`.");
*out_error = true;
return;
}
const auto usedVertsPerInstance = UsedVertsForTFDraw(mode, vertCount);
const auto usedVerts =
CheckedInt<uint32_t>(usedVertsPerInstance) * instanceCount;
const auto remainingCapacity =
mTFO->mActive_VertCapacity - mTFO->mActive_VertPosition;
if (!usedVerts.isValid() || usedVerts.value() > remainingCapacity) {
mWebGL->ErrorInvalidOperation(
"Insufficient buffer capacity remaining for"
" transform feedback.");
*out_error = true;
return;
}
mUsedVerts = usedVerts.value();
}
void Advance() const {
if (!mWithTF) return;
mTFO->mActive_VertPosition += mUsedVerts;
for (const auto& cur : mTFO->mIndexedBindings) {
const auto& buffer = cur.mBufferBinding;
if (buffer) {
buffer->ResetLastUpdateFenceId();
}
}
}
};
static bool HasInstancedDrawing(const WebGLContext& webgl) {
return webgl.IsWebGL2() ||
webgl.IsExtensionEnabled(WebGLExtensionID::ANGLE_instanced_arrays);
}
////////////////////////////////////////
void WebGLContext::DrawArraysInstanced(const GLenum mode, const GLint first,
const GLsizei iVertCount,
const GLsizei instanceCount) {
const FuncScope funcScope(*this, "drawArraysInstanced");
// AUTO_PROFILER_LABEL("WebGLContext::DrawArraysInstanced", GRAPHICS);
if (IsContextLost()) return;
const gl::GLContext::TlsScope inTls(gl);
// -
if (!ValidateNonNegative("first", first) ||
!ValidateNonNegative("vertCount", iVertCount) ||
!ValidateNonNegative("instanceCount", instanceCount)) {
return;
}
const auto vertCount = AssertedCast<uint32_t>(iVertCount);
if (IsWebGL2() && !gl->IsSupported(gl::GLFeature::prim_restart_fixed)) {
MOZ_ASSERT(gl->IsSupported(gl::GLFeature::prim_restart));
if (mPrimRestartTypeBytes != 0) {
mPrimRestartTypeBytes = 0;
// OSX appears to have severe perf issues with leaving this enabled.
gl->fDisable(LOCAL_GL_PRIMITIVE_RESTART);
}
}
// -
const auto fetchLimits = ValidateDraw(this, mode, instanceCount);
if (!fetchLimits) return;
// -
const auto totalVertCount_safe = CheckedInt<uint32_t>(first) + vertCount;
if (!totalVertCount_safe.isValid()) {
ErrorOutOfMemory("`first+vertCount` out of range.");
return;
}
auto totalVertCount = totalVertCount_safe.value();
if (vertCount && instanceCount && totalVertCount > fetchLimits->maxVerts) {
ErrorInvalidOperation(
"Vertex fetch requires %u, but attribs only supply %u.", totalVertCount,
uint32_t(fetchLimits->maxVerts));
return;
}
if (vertCount > mMaxVertIdsPerDraw) {
ErrorOutOfMemory(
"Context's max vertCount is %u, but %u requested. "
"[webgl.max-vert-ids-per-draw]",
mMaxVertIdsPerDraw, vertCount);
return;
}
// -
bool error = false;
// -
const ScopedResolveTexturesForDraw scopedResolve(this, &error);
if (error) return;
const ScopedDrawWithTransformFeedback scopedTF(this, mode, vertCount,
instanceCount, &error);
if (error) return;
// On MacOS (Intel?), `first` in glDrawArrays also increases where instanced
// attribs are fetched from. There are two ways to fix this:
// 1. DrawElements with a [0,1,2,...] index buffer, converting `first` to
// `byteOffset`
// 2. OR offset all non-instanced vertex attrib pointers back, and call
// DrawArrays with first:0.
// * But now gl_VertexID will be wrong! So we inject a uniform to offset it
// back correctly.
// #1 ought to be the lowest overhead for any first>0,
// but DrawElements can't be used with transform-feedback,
// so we need #2 to also work.
// For now, only implement #2.
const auto& activeAttribs = mActiveProgramLinkInfo->active.activeAttribs;
auto driverFirst = first;
if (first && mBug_DrawArraysInstancedUserAttribFetchAffectedByFirst) {
// This is not particularly optimized, but we can if we need to.
bool hasInstancedUserAttrib = false;
bool hasVertexAttrib = false;
for (const auto& a : activeAttribs) {
if (a.location == -1) {
if (a.name == "gl_VertexID") {
hasVertexAttrib = true;
}
continue;
}
const auto& binding = mBoundVertexArray->AttribBinding(a.location);
if (binding.layout.divisor) {
hasInstancedUserAttrib = true;
} else {
hasVertexAttrib = true;
}
}
if (hasInstancedUserAttrib && hasVertexAttrib) {
driverFirst = 0;
}
}
if (driverFirst != first) {
for (const auto& a : activeAttribs) {
if (a.location == -1) continue;
const auto& binding = mBoundVertexArray->AttribBinding(a.location);
if (binding.layout.divisor) continue;
mBoundVertexArray->DoVertexAttrib(a.location, first);
}
gl->fUniform1i(mActiveProgramLinkInfo->webgl_gl_VertexID_Offset, first);
}
{
const auto whatDoesAttrib0Need = WhatDoesVertexAttrib0Need();
auto fakeVertCount = uint64_t(driverFirst) + vertCount;
if (whatDoesAttrib0Need == WebGLVertexAttrib0Status::Default) {
fakeVertCount = 0;
}
if (!(vertCount && instanceCount)) {
fakeVertCount = 0;
}
auto undoAttrib0 = MakeScopeExit([&]() {
MOZ_RELEASE_ASSERT(whatDoesAttrib0Need !=
WebGLVertexAttrib0Status::Default);
UndoFakeVertexAttrib0();
});
if (fakeVertCount) {
if (!DoFakeVertexAttrib0(fakeVertCount, whatDoesAttrib0Need)) {
error = true;
undoAttrib0.release();
}
} else {
// No fake-verts needed.
undoAttrib0.release();
}
ScopedDrawCallWrapper wrapper(*this);
if (vertCount && instanceCount) {
if (HasInstancedDrawing(*this)) {
gl->fDrawArraysInstanced(mode, driverFirst, vertCount, instanceCount);
} else {
MOZ_ASSERT(instanceCount == 1);
gl->fDrawArrays(mode, driverFirst, vertCount);
}
}
}
if (driverFirst != first) {
gl->fUniform1i(mActiveProgramLinkInfo->webgl_gl_VertexID_Offset, 0);
for (const auto& a : activeAttribs) {
if (a.location == -1) continue;
const auto& binding = mBoundVertexArray->AttribBinding(a.location);
if (binding.layout.divisor) continue;
mBoundVertexArray->DoVertexAttrib(a.location, 0);
}
}
Draw_cleanup();
scopedTF.Advance();
}
////////////////////////////////////////
WebGLBuffer* WebGLContext::DrawElements_check(const GLsizei rawIndexCount,
const GLenum type,
const WebGLintptr byteOffset,
const GLsizei instanceCount) {
if (mBoundTransformFeedback && mBoundTransformFeedback->mIsActive &&
!mBoundTransformFeedback->mIsPaused) {
ErrorInvalidOperation(
"DrawElements* functions are incompatible with"
" transform feedback.");
return nullptr;
}
if (!ValidateNonNegative("vertCount", rawIndexCount) ||
!ValidateNonNegative("byteOffset", byteOffset) ||
!ValidateNonNegative("instanceCount", instanceCount)) {
return nullptr;
}
const auto indexCount = uint32_t(rawIndexCount);
uint8_t bytesPerIndex = 0;
switch (type) {
case LOCAL_GL_UNSIGNED_BYTE:
bytesPerIndex = 1;
break;
case LOCAL_GL_UNSIGNED_SHORT:
bytesPerIndex = 2;
break;
case LOCAL_GL_UNSIGNED_INT:
if (IsWebGL2() ||
IsExtensionEnabled(WebGLExtensionID::OES_element_index_uint)) {
bytesPerIndex = 4;
}
break;
}
if (!bytesPerIndex) {
ErrorInvalidEnumInfo("type", type);
return nullptr;
}
if (byteOffset % bytesPerIndex != 0) {
ErrorInvalidOperation(
"`byteOffset` must be a multiple of the size of `type`");
return nullptr;
}
////
if (IsWebGL2() && !gl->IsSupported(gl::GLFeature::prim_restart_fixed)) {
MOZ_ASSERT(gl->IsSupported(gl::GLFeature::prim_restart));
if (mPrimRestartTypeBytes != bytesPerIndex) {
mPrimRestartTypeBytes = bytesPerIndex;
const uint32_t ones = UINT32_MAX >> (32 - 8 * mPrimRestartTypeBytes);
gl->fEnable(LOCAL_GL_PRIMITIVE_RESTART);
gl->fPrimitiveRestartIndex(ones);
}
}
////
// Index fetching
const auto& indexBuffer = mBoundVertexArray->mElementArrayBuffer;
if (!indexBuffer) {
ErrorInvalidOperation("Index buffer not bound.");
return nullptr;
}
const size_t availBytes = indexBuffer->ByteLength();
const auto availIndices =
AvailGroups(availBytes, byteOffset, bytesPerIndex, bytesPerIndex);
if (instanceCount && indexCount > availIndices) {
ErrorInvalidOperation("Index buffer too small.");
return nullptr;
}
return indexBuffer.get();
}
static void HandleDrawElementsErrors(
WebGLContext* webgl, gl::GLContext::LocalErrorScope& errorScope) {
const auto err = errorScope.GetError();
if (err == LOCAL_GL_INVALID_OPERATION) {
webgl->ErrorInvalidOperation(
"Driver rejected indexed draw call, possibly"
" due to out-of-bounds indices.");
return;
}
MOZ_ASSERT(!err);
if (err) {
webgl->ErrorImplementationBug(
"Unexpected driver error during indexed draw"
" call. Please file a bug.");
return;
}
}
void WebGLContext::DrawElementsInstanced(const GLenum mode,
const GLsizei iIndexCount,
const GLenum type,
const WebGLintptr byteOffset,
const GLsizei instanceCount) {
const FuncScope funcScope(*this, "drawElementsInstanced");
// AUTO_PROFILER_LABEL("WebGLContext::DrawElementsInstanced", GRAPHICS);
if (IsContextLost()) return;
const gl::GLContext::TlsScope inTls(gl);
const auto indexBuffer =
DrawElements_check(iIndexCount, type, byteOffset, instanceCount);
if (!indexBuffer) return;
const auto indexCount = AssertedCast<uint32_t>(iIndexCount);
// -
const auto fetchLimits = ValidateDraw(this, mode, instanceCount);
if (!fetchLimits) return;
const auto whatDoesAttrib0Need = WhatDoesVertexAttrib0Need();
uint64_t fakeVertCount = 0;
if (whatDoesAttrib0Need != WebGLVertexAttrib0Status::Default) {
fakeVertCount = fetchLimits->maxVerts;
}
if (!indexCount || !instanceCount) {
fakeVertCount = 0;
}
if (fakeVertCount == UINT64_MAX) { // Ok well that's too many!
const auto exactMaxVertId =
indexBuffer->GetIndexedFetchMaxVert(type, byteOffset, indexCount);
MOZ_RELEASE_ASSERT(exactMaxVertId);
fakeVertCount = uint32_t{*exactMaxVertId};
fakeVertCount += 1;
}
// -
{
uint64_t indexCapacity = indexBuffer->ByteLength();
switch (type) {
case LOCAL_GL_UNSIGNED_BYTE:
break;
case LOCAL_GL_UNSIGNED_SHORT:
indexCapacity /= 2;
break;
case LOCAL_GL_UNSIGNED_INT:
indexCapacity /= 4;
break;
}
uint32_t maxVertId = 0;
const auto isFetchValid = [&]() {
if (!indexCount || !instanceCount) return true;
const auto globalMaxVertId =
indexBuffer->GetIndexedFetchMaxVert(type, 0, indexCapacity);
if (!globalMaxVertId) return true;
if (globalMaxVertId.value() < fetchLimits->maxVerts) return true;
const auto exactMaxVertId =
indexBuffer->GetIndexedFetchMaxVert(type, byteOffset, indexCount);
maxVertId = exactMaxVertId.value();
return maxVertId < fetchLimits->maxVerts;
}();
if (!isFetchValid) {
ErrorInvalidOperation(
"Indexed vertex fetch requires %u vertices, but"
" attribs only supply %u.",
maxVertId + 1, uint32_t(fetchLimits->maxVerts));
return;
}
}
if (indexCount > mMaxVertIdsPerDraw) {
ErrorOutOfMemory(
"Context's max indexCount is %u, but %u requested. "
"[webgl.max-vert-ids-per-draw]",
mMaxVertIdsPerDraw, indexCount);
return;
}
// -
bool error = false;
// -
auto undoAttrib0 = MakeScopeExit([&]() {
MOZ_RELEASE_ASSERT(whatDoesAttrib0Need !=
WebGLVertexAttrib0Status::Default);
UndoFakeVertexAttrib0();
});
if (fakeVertCount) {
if (!DoFakeVertexAttrib0(fakeVertCount, whatDoesAttrib0Need)) {
error = true;
undoAttrib0.release();
}
} else {
// No fake-verts needed.
undoAttrib0.release();
}
// -
const ScopedResolveTexturesForDraw scopedResolve(this, &error);
if (error) return;
{
ScopedDrawCallWrapper wrapper(*this);
{
UniquePtr<gl::GLContext::LocalErrorScope> errorScope;
if (MOZ_UNLIKELY(gl->IsANGLE() &&
gl->mDebugFlags &
gl::GLContext::DebugFlagAbortOnError)) {
// ANGLE does range validation even when it doesn't need to.
// With MOZ_GL_ABORT_ON_ERROR, we need to catch it or hit assertions.
errorScope.reset(new gl::GLContext::LocalErrorScope(*gl));
}
if (indexCount && instanceCount) {
if (HasInstancedDrawing(*this)) {
gl->fDrawElementsInstanced(mode, indexCount, type,
reinterpret_cast<GLvoid*>(byteOffset),
instanceCount);
} else {
MOZ_ASSERT(instanceCount == 1);
gl->fDrawElements(mode, indexCount, type,
reinterpret_cast<GLvoid*>(byteOffset));
}
}
if (errorScope) {
HandleDrawElementsErrors(this, *errorScope);
}
}
}
Draw_cleanup();
}
////////////////////////////////////////
void WebGLContext::Draw_cleanup() {
if (gl->WorkAroundDriverBugs()) {
if (gl->Renderer() == gl::GLRenderer::Tegra) {
mDrawCallsSinceLastFlush++;
if (mDrawCallsSinceLastFlush >= MAX_DRAW_CALLS_SINCE_FLUSH) {
gl->fFlush();
mDrawCallsSinceLastFlush = 0;
}
}
}
// Let's check for a really common error: Viewport is larger than the actual
// destination framebuffer.
uint32_t destWidth;
uint32_t destHeight;
if (mBoundDrawFramebuffer) {
const auto& info = mBoundDrawFramebuffer->GetCompletenessInfo();
destWidth = info->width;
destHeight = info->height;
} else {
destWidth = mDefaultFB->mSize.width;
destHeight = mDefaultFB->mSize.height;
}
if (mViewportWidth > int32_t(destWidth) ||
mViewportHeight > int32_t(destHeight)) {
if (!mAlreadyWarnedAboutViewportLargerThanDest) {
GenerateWarning(
"Drawing to a destination rect smaller than the viewport"
" rect. (This warning will only be given once)");
mAlreadyWarnedAboutViewportLargerThanDest = true;
}
}
}
WebGLVertexAttrib0Status WebGLContext::WhatDoesVertexAttrib0Need() const {
MOZ_ASSERT(mCurrentProgram);
MOZ_ASSERT(mActiveProgramLinkInfo);
bool legacyAttrib0 = mNeedsLegacyVertexAttrib0Handling;
if (gl->WorkAroundDriverBugs() && kIsMacOS) {
// Also programs with no attribs:
// conformance/attribs/gl-vertex-attrib-unconsumed-out-of-bounds.html
const auto& activeAttribs = mActiveProgramLinkInfo->active.activeAttribs;
bool hasNonInstancedUserAttrib = false;
for (const auto& a : activeAttribs) {
if (a.location == -1) continue;
const auto& layout = mBoundVertexArray->AttribBinding(a.location).layout;
if (layout.divisor == 0) {
hasNonInstancedUserAttrib = true;
}
}
legacyAttrib0 |= !hasNonInstancedUserAttrib;
}
if (!legacyAttrib0) return WebGLVertexAttrib0Status::Default;
MOZ_RELEASE_ASSERT(mMaybeNeedsLegacyVertexAttrib0Handling,
"Invariant need because this turns on index buffer "
"validation, needed for fake-attrib0.");
if (!mActiveProgramLinkInfo->attrib0Active) {
// Attrib0 unused, so just ensure that the legacy code has enough buffer.
return WebGLVertexAttrib0Status::EmulatedUninitializedArray;
}
const auto& isAttribArray0Enabled =
mBoundVertexArray->AttribBinding(0).layout.isArray;
return isAttribArray0Enabled
? WebGLVertexAttrib0Status::Default
: WebGLVertexAttrib0Status::EmulatedInitializedArray;
}
bool WebGLContext::DoFakeVertexAttrib0(
const uint64_t fakeVertexCount,
const WebGLVertexAttrib0Status whatDoesAttrib0Need) {
MOZ_ASSERT(fakeVertexCount);
MOZ_RELEASE_ASSERT(whatDoesAttrib0Need != WebGLVertexAttrib0Status::Default);
if (gl->WorkAroundDriverBugs() && gl->IsMesa()) {
// Padded/strided to vec4, so 4x4bytes.
const auto effectiveVertAttribBytes =
CheckedInt<int32_t>(fakeVertexCount) * 4 * 4;
if (!effectiveVertAttribBytes.isValid()) {
ErrorOutOfMemory("`offset + count` too large for Mesa.");
return false;
}
}
if (!mAlreadyWarnedAboutFakeVertexAttrib0) {
GenerateWarning(
"Drawing without vertex attrib 0 array enabled forces the browser "
"to do expensive emulation work when running on desktop OpenGL "
"platforms, for example on Mac. It is preferable to always draw "
"with vertex attrib 0 array enabled, by using bindAttribLocation "
"to bind some always-used attribute to location 0.");
mAlreadyWarnedAboutFakeVertexAttrib0 = true;
}
gl->fEnableVertexAttribArray(0);
{
const auto& attrib0 = mBoundVertexArray->AttribBinding(0);
if (attrib0.layout.divisor) {
gl->fVertexAttribDivisor(0, 0);
}
}
if (!mFakeVertexAttrib0BufferObject) {
gl->fGenBuffers(1, &mFakeVertexAttrib0BufferObject);
mFakeVertexAttrib0BufferObjectSize = 0;
}
gl->fBindBuffer(LOCAL_GL_ARRAY_BUFFER, mFakeVertexAttrib0BufferObject);
////
switch (mGenericVertexAttribTypes[0]) {
case webgl::AttribBaseType::Boolean:
case webgl::AttribBaseType::Float:
gl->fVertexAttribPointer(0, 4, LOCAL_GL_FLOAT, false, 0, 0);
break;
case webgl::AttribBaseType::Int:
gl->fVertexAttribIPointer(0, 4, LOCAL_GL_INT, 0, 0);
break;
case webgl::AttribBaseType::Uint:
gl->fVertexAttribIPointer(0, 4, LOCAL_GL_UNSIGNED_INT, 0, 0);
break;
}
////
const auto maxFakeVerts = StaticPrefs::webgl_fake_verts_max();
if (fakeVertexCount > maxFakeVerts) {
ErrorOutOfMemory(
"Draw requires faking a vertex attrib 0 array, but required vert count"
" (%" PRIu64 ") is more than webgl.fake-verts.max (%u).",
fakeVertexCount, maxFakeVerts);
return false;
}
const auto bytesPerVert = sizeof(mFakeVertexAttrib0Data);
const auto checked_dataSize =
CheckedInt<intptr_t>(fakeVertexCount) * bytesPerVert;
if (!checked_dataSize.isValid()) {
ErrorOutOfMemory(
"Integer overflow trying to construct a fake vertex attrib 0"
" array for a draw-operation with %" PRIu64
" vertices. Try"
" reducing the number of vertices.",
fakeVertexCount);
return false;
}
const auto dataSize = checked_dataSize.value();
if (mFakeVertexAttrib0BufferObjectSize < dataSize) {
gl::GLContext::LocalErrorScope errorScope(*gl);
gl->fBufferData(LOCAL_GL_ARRAY_BUFFER, dataSize, nullptr,
LOCAL_GL_DYNAMIC_DRAW);
const auto err = errorScope.GetError();
if (err) {
ErrorOutOfMemory(
"Failed to allocate fake vertex attrib 0 data: %zi bytes", dataSize);
return false;
}
mFakeVertexAttrib0BufferObjectSize = dataSize;
mFakeVertexAttrib0DataDefined = false;
}
if (whatDoesAttrib0Need ==
WebGLVertexAttrib0Status::EmulatedUninitializedArray)
return true;
////
if (mFakeVertexAttrib0DataDefined &&
memcmp(mFakeVertexAttrib0Data, mGenericVertexAttrib0Data, bytesPerVert) ==
0) {
return true;
}
////
const auto data = UniqueBuffer::Take(malloc(dataSize));
if (!data) {
ErrorOutOfMemory("Failed to allocate fake vertex attrib 0 array.");
return false;
}
auto itr = (uint8_t*)data.get();
const auto itrEnd = itr + dataSize;
while (itr != itrEnd) {
memcpy(itr, mGenericVertexAttrib0Data, bytesPerVert);
itr += bytesPerVert;
}
{
gl::GLContext::LocalErrorScope errorScope(*gl);
gl->fBufferSubData(LOCAL_GL_ARRAY_BUFFER, 0, dataSize, data.get());
const auto err = errorScope.GetError();
if (err) {
ErrorOutOfMemory("Failed to upload fake vertex attrib 0 data.");
return false;
}
}
////
memcpy(mFakeVertexAttrib0Data, mGenericVertexAttrib0Data, bytesPerVert);
mFakeVertexAttrib0DataDefined = true;
return true;
}
void WebGLContext::UndoFakeVertexAttrib0() {
static_assert(IsBufferTargetLazilyBound(LOCAL_GL_ARRAY_BUFFER));
mBoundVertexArray->DoVertexAttrib(0);
}
} // namespace mozilla