import renderdoc as rd
import rdtest
import pprint
import math
from typing import List


def float_value(x): return x.floatValue
def uint_value(x): return x.uintValue
def unknown_stencil(x): return x == -2

# Not a real test, re-used by API-specific tests


class Pixel_History(rdtest.TestCase):
    internal = True
    demos_test_name = None

    def check_capture(self):
        # cache some information since our python bindings deep copy actions and this can add up
        self.eventCache = {}
        for a in self.controller.GetRootActions():
            self.populate_eventcache(a, '', [])

        self.errored = False

        self.batch_base = self.find_action("Batch:", 0)
        while self.batch_base is not None:
            with rdtest.log.auto_section(self.batch_base.customName):
                self.is_secondary = 'Secondary' in self.batch_base.customName
                self.batch_test(self.batch_base.eventId)
                self.batch_base = self.find_action(
                    "Batch:", self.batch_base.eventId+1)
                
        if self.errored:
            raise rdtest.TestFailureException("Detected problems in pixel history")

    def error(self, message):
        rdtest.log.error(message)
        self.errored = True

    def relative_xy(self, x, y):
        self.x, self.y = (x >> self.sub.mip, y >> self.sub.mip)
        return (self.x, self.y)

    def populate_eventcache(self, action: rd.ActionDescription, name: str, parents: List[int]):
        hierarchy = parents + [action.eventId]

        self.eventCache[action.eventId] = {
            'name': name,
            'flags': action.flags,
            'hierarchy': hierarchy,
        }

        for child in action.children:
            if child.flags & rd.ActionFlags.SetMarker:
                name = child.customName

            self.populate_eventcache(child, name, hierarchy)

    def get_eventname(self, eventId: int) -> str:
        return self.eventCache[eventId]['name']

    def get_actionflags(self, eventId: int) -> rd.ActionFlags:
        return self.eventCache[eventId]['flags']

    def get_hierarchy(self, eventId: int) -> List[int]:
        return self.eventCache[eventId]['hierarchy']

    def batch_test(self, base_eid: int):
        test_marker = self.find_action("Simple Test", base_eid)
        self.controller.SetFrameEvent(test_marker.next.eventId, True)

        pipe = self.controller.GetPipelineState()

        if len(pipe.GetOutputTargets()) > 0:
            col = pipe.GetOutputTargets()[0]
        else:
            col = None
        depth = pipe.GetDepthTarget()

        self.has_colour = (col is not None and col.resource != rd.ResourceId())
        self.has_depth = (depth.resource != rd.ResourceId())

        if self.has_colour:
            rt = col
            tex = col.resource
            self.is_depth = False
        else:
            self.check(self.has_depth)
            self.is_depth = True
            rt = depth

        if self.has_depth:
            depth_details = self.get_texture(depth.resource)
            self.has_stencil = (depth_details.format.type in [
                                rd.ResourceFormatType.D16S8, rd.ResourceFormatType.D24S8, rd.ResourceFormatType.D32S8])
        else:
            self.has_stencil = False

        self.tex = tex = rt.resource
        tex_details = self.get_texture(tex)

        self.epsilon = 1.0/float(1 << tex_details.format.compByteWidth)

        sub = rd.Subresource()
        if tex_details.arraysize > 1 or tex_details.depth > 1:
            sub.slice = rt.firstSlice
        if tex_details.mips > 1:
            sub.mip = rt.firstMip

        self.sub = sub
        self.comp = comp = rt.format.compType

        def event_name(x: rd.PixelModification):
            return self.get_eventname(x.eventId)

        value_func = float_value
        alpha_value = 2.75  # 1 + ALPHA_ADD=1.75
        overflow_value = 100000.0
        clear_col = 0.2

        if self.comp == rd.CompType.UInt:
            alpha_value = 4  # 1 + ALPHA_ADD=3
            clear_col = 5  # uint values are shifted by 16, we clear to 80,80,80,160
            value_func = uint_value
            overflow_value = (1 << 28)-1

        def roundToInf(col):
            # for half-floats lower inf to the largest representable value since either is valid
            if tex_details.format.compType == rd.CompType.Float and tex_details.format.compByteWidth == 2:
                return tuple([65504.0 if x == math.inf else x for x in col])

            return col

        self.fetch_property = {
            '_': None,  # dynamically skipped test via with_col or with_depth below
            'event_name': event_name,
            'value': value_func,
            'passed': lambda x: x.Passed(),
            'culled': lambda x: x.backfaceCulled,
            'depth_test_failed': lambda x: x.depthTestFailed,
            'depth_clipped': lambda x: x.depthClipped,
            'sample_masked': lambda x: x.sampleMasked,
            'depth_bounds_failed': lambda x: x.depthBoundsFailed,
            'scissor_clipped': lambda x: x.scissorClipped,
            'stencil_test_failed': lambda x: x.stencilTestFailed,
            'shader_discarded': lambda x: x.shaderDiscarded,
            'shader_out_col': lambda x: value_func(x.shaderOut.col),
            'shader_out_depth': lambda x: x.shaderOut.depth,
            'pre_mod_col': lambda x: value_func(x.preMod.col),
            'post_mod_col': lambda x: roundToInf(value_func(x.postMod.col)),
            'pre_mod_depth': lambda x: x.preMod.depth,
            'post_mod_depth': lambda x: x.postMod.depth,
            'post_mod_stencil': lambda x: x.postMod.stencil,
            'primitive_id': lambda x: x.primitiveID,
            'unknown_post_mod_stencil': lambda x: unknown_stencil(x.postMod.stencil),
            'unboundPS': lambda x: x.unboundPS,
            'directWrite': lambda x: x.directShaderWrite,
        }

        def with_depth(x):
            return x if self.has_depth else '_'

        def with_stencil(x):
            return x if self.has_stencil else '_'

        def fmt_adjusted(r, g, b, a):
            if tex_details.format.compType == rd.CompType.UInt:
                r = int(r * 16)
                g = int(g * 16)
                b = int(b * 16)
                a = int(a * 16)

            return (r, g, b, a)

        def fmt_clamped(r, g, b, a):
            r, g, b, a = fmt_adjusted(r, g, b, a)

            if tex_details.format.compType == rd.CompType.UNorm:
                r = max(0, min(1, r))
                g = max(0, min(1, g))
                b = max(0, min(1, b))
                a = max(0, min(1, a))
            elif tex_details.format.compType == rd.CompType.Float and tex_details.format.compByteWidth == 2:
                r = max(-65504, min(65504, r))
                g = max(-65504, min(65504, g))
                b = max(-65504, min(65504, b))
                a = max(-65504, min(65504, a))
            elif tex_details.format.compType == rd.CompType.UInt:
                maxval = (1 << (8*tex_details.format.compByteWidth))-1
                r = max(0, min(maxval, r))
                b = max(0, min(maxval, b))
                a = max(0, min(maxval, a))
                g = max(0, min(maxval, g))

            return r, g, b, a

        # can't reliably get colours, depths, or primitive IDs on secondaries
        if self.is_secondary:
            self.fetch_property['pre_mod_col'] = None
            self.fetch_property['post_mod_col'] = None
            self.fetch_property['shader_out_col'] = None
            self.fetch_property['pre_mod_depth'] = None
            self.fetch_property['shader_out_depth'] = None
            self.fetch_property['post_mod_depth'] = None
            self.fetch_property['post_mod_stencil'] = None
            self.fetch_property['unknown_post_mod_stencil'] = None
            self.fetch_property['primitive_id'] = None

        if not self.has_colour:
            self.fetch_property['pre_mod_col'] = None
            self.fetch_property['post_mod_col'] = None
            self.fetch_property['shader_out_col'] = None

        if not self.has_depth:
            self.fetch_property['depth_test_failed'] = None
            self.fetch_property['depth_clipped'] = None
            self.fetch_property['depth_bounds_failed'] = None
            self.fetch_property['stencil_test_failed'] = None
            self.fetch_property['pre_mod_depth'] = None
            self.fetch_property['shader_out_depth'] = None
            self.fetch_property['post_mod_depth'] = None
            self.fetch_property['post_mod_stencil'] = None
            self.fetch_property['unknown_post_mod_stencil'] = None

        # don't check stencil if there's no stencil channel
        if not self.has_stencil:
            self.fetch_property['post_mod_stencil'] = None
            self.fetch_property['unknown_post_mod_stencil'] = None

        # check if depth bounds is supportted - D3D11 does not
        has_depth_bounds = self.find_action('Depth Bounds Prep') is not None

        x, y = self.relative_xy(110, 100)
        rdtest.log.print("Testing Unbound PS {}, {}".format(x, y))
        modifs = self.controller.PixelHistory(tex, x, y, sub, comp)
        events = [
            {
                'event_name': 'Begin RenderPass',
                'passed': True
            },
            {
                'event_name': 'Unbound Shader',
                'passed': True,
                'unboundPS': True,
                'primitive_id': 0,
                'post_mod_stencil': 0x33
            },
            {
                'event_name': 'Stencil Write',
                'passed': True,
                'primitive_id': 0,
                'post_mod_stencil': 0x55
            },
        ]
        self.check_events(events, modifs)

        x, y = self.relative_xy(170, 140)
        rdtest.log.print("Testing Stencil failure {}, {}".format(x, y))
        modifs = self.controller.PixelHistory(tex, x, y, sub, comp)
        events = [
            {
                'event_name': 'Begin RenderPass',
                'passed': True
            },
            {
                'event_name': 'Unbound Shader',
                'passed': True,
                'unboundPS': True,
                'primitive_id': 0
            },
            {
                'event_name': 'Stencil Write',
                'passed': True
            },
            {
                'event_name': 'Background',
                'depth_test_failed': True,
                with_depth('post_mod_col'): fmt_clamped(1, 0, 0, alpha_value)
            },
            {
                'event_name': 'Simple Test',
                with_stencil('stencil_test_failed'): True
            },
        ]
        self.check_events(events, modifs)

        x, y = self.relative_xy(170, 160)
        rdtest.log.print("Testing Depth failure {}, {}".format(x, y))
        modifs = self.controller.PixelHistory(tex, x, y, sub, comp)
        events = [
            {
                'event_name': 'Begin RenderPass',
                'passed': True
            },
            {
                'event_name': 'Depth Write',
                'passed': True
            },
            {
                'event_name': 'Background',
                'depth_test_failed': True
            },
            {
                'event_name': 'Cull Front',
                'culled': True
            },
            {
                'event_name': 'Simple Test',
                'depth_test_failed': True
            },
        ]
        self.check_events(events, modifs)

        x, y = self.relative_xy(150, 250)
        rdtest.log.print("Testing Discard {}, {}".format(x, y))
        modifs = self.controller.PixelHistory(tex, x, y, sub, comp)
        events = [
            {
                'event_name': 'Begin RenderPass',
                'passed': True
            },
            {
                'event_name': 'Background',
                'shader_discarded': True
            },
        ]
        self.check_events(events, modifs)

        x, y = self.relative_xy(330, 145)
        rdtest.log.print("Testing Primitive ID {}, {}".format(x, y))
        modifs = self.controller.PixelHistory(tex, x, y, sub, comp)
        events = [
            {
                'event_name': 'Begin RenderPass',
                'passed': True
            },
            {
                'event_name': 'Simple Test',
                'passed': True,
                'primitive_id': 3,
                'shader_out_col': fmt_adjusted(0, 0, 0, alpha_value)
            },
        ]
        self.check_events(events, modifs)

        x, y = self.relative_xy(340, 145)
        rdtest.log.print("Testing Depth Clip {}, {}".format(x, y))
        modifs = self.controller.PixelHistory(tex, x, y, sub, comp)
        events = [
            {
                'event_name': 'Begin RenderPass',
                'passed': True
            },
            {
                'event_name': 'Simple Test',
                'passed': False,
                'depth_clipped': True
            },
        ]
        self.check_events(events, modifs)

        # if depth bounds test isn't supported these draws won't be emitted
        if has_depth_bounds:
            x, y = self.relative_xy(330, 102)
            rdtest.log.print("Testing Depth Bounds Pass {}, {}".format(x, y))
            modifs = self.controller.PixelHistory(tex, x, y, sub, comp)
            events = [
                {
                    'event_name': 'Begin RenderPass',
                    'passed': True
                },
                {
                    'event_name': 'Depth Bounds Prep',
                    'passed': True,
                    'primitive_id': 0,
                    'shader_out_col': fmt_adjusted(1, 0, 0, alpha_value)
                },
                {
                    'event_name': 'Depth Bounds Clip',
                    'passed': True,
                    'primitive_id': 0,
                    'shader_out_col': fmt_adjusted(0, 1, 0, alpha_value)
                },
            ]
            self.check_events(events, modifs)

            # slightly precise X to ensure it still picks the right edge of the triangle on mip version
            x, y = self.relative_xy(318, 102)
            rdtest.log.print("Testing Depth Bounds Fail {}, {}".format(x, y))
            modifs = self.controller.PixelHistory(tex, x, y, sub, comp)
            events = [
                {
                    'event_name': 'Begin RenderPass',
                    'passed': True
                },
                {
                    'event_name': 'Depth Bounds Prep',
                    'passed': True,
                    'primitive_id': 0,
                    'shader_out_col': fmt_adjusted(1, 0, 0, alpha_value)
                },
                {
                    'event_name': 'Depth Bounds Clip',
                    with_depth('passed'): False,
                    'depth_bounds_failed': True
                },
            ]
            self.check_events(events, modifs)

            x, y = self.relative_xy(346, 102)
            rdtest.log.print("Testing Depth Bounds Fail {}, {}".format(x, y))
            modifs = self.controller.PixelHistory(tex, x, y, sub, comp)
            events = [
                {
                    'event_name': 'Begin RenderPass',
                    'passed': True
                },
                {
                    'event_name': 'Depth Bounds Prep',
                    'passed': True,
                    'primitive_id': 0,
                    'shader_out_col': fmt_adjusted(1, 0, 0, alpha_value)
                },
                {
                    'event_name': 'Depth Bounds Clip',
                    with_depth('passed'): False,
                    'depth_bounds_failed': True
                },
            ]
            self.check_events(events, modifs)

        rdtest.log.print("Testing stencil/scissor checks")
        test_marker = self.find_action("Stencil Mask", base_eid)
        self.controller.SetFrameEvent(test_marker.next.eventId, True)

        x, y = self.relative_xy(106, 248)
        rdtest.log.print("Testing pixel {}, {}".format(x, y))
        modifs = self.controller.PixelHistory(tex, x, y, sub, comp)
        events = [
            {
                'event_name': 'Begin RenderPass',
                'passed': True
            },
            {
                'event_name': 'Background',
                'passed': True
            },
            {
                'event_name': 'Scissor Fail',
                'scissor_clipped': True
            },
            {
                'event_name': 'Scissor Pass',
                'passed': True,
                'shader_out_col': fmt_adjusted(0, 1, 0, alpha_value),
                'post_mod_col': fmt_clamped(0, 1, 0, alpha_value)
            },
            {
                'event_name': 'Stencil Ref',
                'passed': True,
                'shader_out_col': fmt_adjusted(0, 0, 1, alpha_value),
                'post_mod_col': fmt_clamped(0, 0, 1, alpha_value)
            },
            {
                'event_name': 'Stencil Mask',
                'passed': True,
                'shader_out_col': fmt_adjusted(0, 1, 1, alpha_value),
                'post_mod_col': fmt_clamped(0, 1, 1, alpha_value)
            },
        ]
        self.check_events(events, modifs)

        rdtest.log.print("Testing depth test for per fragment reporting")
        test_marker = self.find_action("Depth Test", base_eid)
        self.controller.SetFrameEvent(test_marker.next.eventId, True)

        x, y = self.relative_xy(275, 258)
        rdtest.log.print("Testing pixel {}, {}".format(x, y))
        modifs = self.controller.PixelHistory(tex, x, y, sub, comp)
        events = [
            {
                'event_name': 'Begin RenderPass',
                'passed': True
            },
            {
                'event_name': 'Background',
                'passed': True
            },
            {
                'event_name': 'Depth Test',
                'primitive_id': 0,
                'depth_test_failed': True,
                'shader_out_col': fmt_adjusted(0, 1, 0, alpha_value),
                'shader_out_depth': 0.97,
                with_depth('post_mod_col'): fmt_clamped(1, 0, 1, alpha_value),
                'post_mod_depth': 0.95,
                'unknown_post_mod_stencil': True
            },
            {
                'event_name': 'Depth Test',
                'passed': True,
                'primitive_id': 1,
                'depth_test_failed': False,
                'shader_out_col': fmt_adjusted(1, 1, 0, alpha_value),
                'shader_out_depth': 0.20,
                'post_mod_col': fmt_clamped(1, 1, 0, alpha_value),
                'post_mod_depth': 0.20,
                'unknown_post_mod_stencil': True
            },
            {
                'event_name': 'Depth Test',
                'primitive_id': 2,
                'depth_test_failed': True,
                'shader_out_col': fmt_adjusted(1, 0, 0, alpha_value),
                'shader_out_depth': 0.30,
                with_depth('post_mod_col'): fmt_clamped(1, 1, 0, alpha_value),
                'post_mod_depth': 0.20,
                'unknown_post_mod_stencil': True
            },
            {
                'event_name': 'Depth Test',
                'passed': True,
                'primitive_id': 3,
                'depth_test_failed': False,
                'shader_out_col': fmt_adjusted(0, 0, 1, alpha_value),
                'shader_out_depth': 0.10,
                'post_mod_col': fmt_clamped(0, 0, 1, alpha_value),
                'post_mod_depth': 0.10,
                'unknown_post_mod_stencil': True
            },
        ]

        # if we don't have depth bounds testing the final event will pass
        if has_depth_bounds:
            events.append(
                {
                    'event_name': 'Depth Test',
                    'primitive_id': 4,
                    'depth_test_failed': False,
                    'depth_bounds_failed': True,
                    'shader_out_col': fmt_adjusted(1, 1, 1, alpha_value),
                    'shader_out_depth': 0.05,
                    with_depth('post_mod_col'): fmt_clamped(0, 0, 1, alpha_value),
                    'post_mod_depth': 0.10,
                    'unknown_post_mod_stencil': False
                })
        else:
            events.append(
                {
                    'event_name': 'Depth Test',
                    'passed': True,
                    'primitive_id': 4,
                    'depth_test_failed': False,
                    'shader_out_col': fmt_adjusted(1, 1, 1, alpha_value),
                    'shader_out_depth': 0.05,
                    with_depth('post_mod_col'): fmt_clamped(1, 1, 1, alpha_value),
                    'post_mod_depth': 0.05,
                    'unknown_post_mod_stencil': False
                })

        # can't distinguish per-fragment results in secondaries
        if self.is_secondary:
            events = [
                {
                    'event_name': 'Begin RenderPass',
                    'passed': True
                },
                {
                    'event_name': 'Background',
                    'passed': True
                },
                {
                    'event_name': 'Depth Test',
                    'post_mod_col': fmt_clamped(0, 0, 1, alpha_value),
                },
            ]
        self.check_events(events, modifs)

        # For pixel 60, 130 inside the light green triangle which is 300 draws of 1 instance of 1 triangle
        rdtest.log.print("Testing Lots of Drawcalls")
        test_marker = self.find_action("300 Instances", base_eid)
        self.controller.SetFrameEvent(test_marker.next.eventId, True)
        x, y = self.relative_xy(60, 130)
        rdtest.log.print("Testing pixel {}, {}".format(x, y))
        modifs = self.controller.PixelHistory(tex, x, y, sub, comp)
        
        if not self.is_secondary:
            self.check_final_colour(tex, x, y, modifs, sub, comp)

        countEvents = 1 + 300
        if len(modifs) != countEvents:
            self.error("Expected {} events, got {}".format(countEvents, len(modifs)))
        self.check_modifs_consistent(modifs)

        # For pixel 60, 50 inside the orange triangle which is 1 draws of 300 instances of 1 triangle
        rdtest.log.print("Testing Lots of Instances")
        x, y = self.relative_xy(60, 50)
        rdtest.log.print("Testing pixel {}, {}".format(x, y))
        modifs = self.controller.PixelHistory(tex, x, y, sub, comp)
        if not self.is_secondary:
            self.check_final_colour(tex, x, y, modifs, sub, comp)

        countEvents = 1 + 255
        # secondaries can't count fragment events, so we only get 1 for the draw
        if self.is_secondary:
            countEvents = 1 + 1
        if len(modifs) != countEvents:
            self.error("Expected {} events, got {}".format(countEvents, len(modifs)))
        self.check_modifs_consistent(modifs)

        rdtest.log.print("Testing Sample colouring")
        test_marker = self.find_action("Sample Colouring", base_eid)
        self.controller.SetFrameEvent(test_marker.next.eventId, True)
        x, y = self.relative_xy(330, 200)
        rdtest.log.print("Testing pixel {}, {}".format(x, y))
        modifs = self.controller.PixelHistory(tex, x, y, sub, comp)
        events = [
            {
                'event_name': 'Begin RenderPass',
                'passed': True
            },
            {
                'event_name': 'Sample Colouring',
                'passed': True,
                'primitive_id': 0,
                'shader_out_col': fmt_adjusted(1, 0, 0, alpha_value)
            },
        ]
        self.check_events(events, modifs)
        self.check_modifs_consistent(modifs)

        if tex_details.msSamp == 4:
            sub.sample = 1
            rdtest.log.print(f"Testing sample {sub.sample}")
            modifs = self.controller.PixelHistory(tex, x, y, sub, comp)
            events = [
                {
                    'event_name': 'Begin RenderPass',
                    'passed': True
                },
                {
                    'event_name': 'Sample Colouring',
                    'passed': True,
                    'primitive_id': 0,
                    'shader_out_col': fmt_adjusted(0, 0, 1, alpha_value)
                },
            ]
            self.check_events(events, modifs)
            self.check_modifs_consistent(modifs)

            sub.sample = 2
            rdtest.log.print(f"Testing sample {sub.sample}")
            modifs = self.controller.PixelHistory(tex, x, y, sub, comp)
            events = [
                {
                    'event_name': 'Begin RenderPass',
                    'passed': True
                },
                {
                    'event_name': 'Sample Colouring',
                    'passed': True,
                    'primitive_id': 0,
                    'shader_out_col': fmt_adjusted(0, 1, 1, alpha_value)
                },
            ]
            self.check_events(events, modifs)
            self.check_modifs_consistent(modifs)

            sub.sample = 3
            rdtest.log.print(f"Testing sample {sub.sample}")
            modifs = self.controller.PixelHistory(tex, x, y, sub, comp)
            events = [
                {
                    'event_name': 'Begin RenderPass',
                    'passed': True,
                },
                {
                    'event_name': 'Sample Colouring',
                    'passed': False,
                    'sample_masked': True,
                },
            ]
            self.check_events(events, modifs)
            self.check_modifs_consistent(modifs)

            sub.sample = 0

        rdtest.log.print("Testing depth-equal testing")
        test_marker = self.find_action("Depth Equal Pass", base_eid)
        self.controller.SetFrameEvent(test_marker.next.eventId, True)

        x, y = self.relative_xy(200, 250)
        rdtest.log.print("Testing pixel {}, {}".format(x, y))
        modifs = self.controller.PixelHistory(tex, x, y, sub, comp)
        events = [
            {
                'event_name': 'Begin RenderPass',
                'passed': True
            },
            {
                'event_name': 'Depth Equal Setup',
                'passed': True,
                'shader_out_col': fmt_adjusted(0.1, 0.1, 0.1, alpha_value),
                'shader_out_depth': 0.1,
                with_depth('post_mod_col'): fmt_clamped(0.1, 0.1, 0.1, alpha_value),
            },
            {
                'event_name': 'Background',
                with_depth('passed'): False,
                'depth_test_failed': True,
            },
            {
                'event_name': 'Depth Equal Fail',
                with_depth('passed'): False,
                'depth_test_failed': True,
                'shader_out_col': fmt_adjusted(0, 0, 0, alpha_value),
                'shader_out_depth': 0.1 + 5.0e-4,
                with_depth('post_mod_col'): fmt_clamped(0.1, 0.1, 0.1, alpha_value),
            },
            {
                'event_name': 'Depth Equal Pass',
                'passed': True,
                'shader_out_col': fmt_adjusted(1, 1, 1, alpha_value),
                'shader_out_depth': 0.1 + 1e-8,
                'post_mod_col': fmt_clamped(1, 1, 1, alpha_value),
            },
        ]
        self.check_events(events, modifs)

        rdtest.log.print("Testing colour masking")
        test_marker = self.find_action("Colour Masked", base_eid)
        self.controller.SetFrameEvent(test_marker.next.eventId, True)

        x, y = self.relative_xy(60, 80)
        rdtest.log.print("Testing pixel {}, {}".format(x, y))
        modifs = self.controller.PixelHistory(tex, x, y, sub, comp)
        events = [
            {
                'event_name': 'Begin RenderPass',
                'passed': True
            },
            {
                'event_name': 'Colour Masked',
                'passed': True,
                'shader_out_col': fmt_adjusted(3, 3, 3, 2+alpha_value),
                'post_mod_col': fmt_clamped(3, 3, clear_col, 1),
            },
        ]
        self.check_events(events, modifs)

        if self.has_colour:
            rdtest.log.print("Testing direct writes")
            test_marker = self.find_action("Compute write", base_eid)

            # don't have compute writes in secondaries and some tests like D3D MSAA won't have compute writes
            if test_marker is not None and not self.is_secondary and base_eid in self.get_hierarchy(test_marker.eventId):
                self.controller.SetFrameEvent(test_marker.next.eventId, True)
                x, y = self.relative_xy(225, 85)
                rdtest.log.print("Testing pixel {}, {}".format(x, y))
                modifs = self.controller.PixelHistory(tex, x, y, sub, comp)
                events = [
                    {
                        'event_name': 'Begin RenderPass',
                        'passed': True
                    },
                    {
                        'event_name': 'Background',
                        'passed': True
                    },
                    {
                        'event_name': 'Compute write',
                        'passed': True,
                        'directWrite': True,
                        'shader_out_depth': -1,
                        'post_mod_stencil': -1,
                        'post_mod_depth': -1,
                        'post_mod_col': fmt_clamped(3, 3, 3, 9),
                    },
                ]
                self.check_events(events, modifs)

            rdtest.log.print("Testing overflowed writes")
            test_marker = self.find_action("Overflowing", base_eid)
            self.controller.SetFrameEvent(test_marker.next.eventId, True)

            x, y = self.relative_xy(105, 50)
            rdtest.log.print("Testing pixel {}, {}".format(x, y))
            modifs = self.controller.PixelHistory(tex, x, y, sub, comp)
            events = [
                {
                    'event_name': 'Begin RenderPass',
                    'passed': True
                },
                {
                    'event_name': 'Overflowing',
                    'passed': True,
                    'shader_out_col': fmt_adjusted(overflow_value, 0, 0, alpha_value),
                    'post_mod_col': fmt_clamped(overflow_value, 0, 0, alpha_value)
                },
                {
                    'event_name': 'Overflowing',
                    'passed': True,
                    'shader_out_col': fmt_adjusted(0, overflow_value, 0, alpha_value),
                    'post_mod_col': fmt_clamped(0, overflow_value, 0, alpha_value)
                },
                {
                    'event_name': 'Overflowing',
                    'passed': True,
                    'shader_out_col': fmt_adjusted(0, 0, overflow_value, alpha_value),
                    'post_mod_col': fmt_clamped(0, 0, overflow_value, alpha_value)
                },
            ]
            # can't distinguish per-fragment results in secondaries
            if self.is_secondary:
                events = [
                    {
                        'event_name': 'Begin RenderPass',
                        'passed': True
                    },
                    {
                        'event_name': 'Overflowing',
                        'passed': True,
                        'shader_out_col': fmt_adjusted(0, 0, overflow_value, alpha_value),
                        'post_mod_col': fmt_clamped(0, 0, overflow_value, alpha_value)
                    },
                ]
            self.check_events(events, modifs)

        rdtest.log.print("Testing per-fragment discards")
        test_marker = self.find_action("Per-Fragment discarding", base_eid)
        self.controller.SetFrameEvent(test_marker.next.eventId, True)

        x, y = self.relative_xy(60, 160)
        rdtest.log.print("Testing pixel {}, {}".format(x, y))
        modifs = self.controller.PixelHistory(tex, x, y, sub, comp)
        events = [
            {
                'event_name': 'Begin RenderPass',
                'passed': True
            },
            {
                'event_name': 'Per-Fragment discarding',
                'passed': False,
                'shader_discarded': True,
                'shader_out_col': (0, 0, 0, 0),
                'shader_out_depth': -1,
                'unknown_post_mod_stencil': True,
                'primitive_id': 0,
            },
            {
                'event_name': 'Per-Fragment discarding',
                'passed': True,
                'primitive_id': 1,
                'shader_out_col': fmt_adjusted(1, 1, 1, alpha_value),
                'shader_out_depth': 0.33,
                'post_mod_col': fmt_clamped(1, 1, 1, alpha_value),
            },
        ]
        # can't distinguish per-fragment results in secondaries
        if self.is_secondary:
            events = [
                {
                    'event_name': 'Begin RenderPass',
                    'passed': True
                },
                {
                    'event_name': 'Per-Fragment discarding',
                    'passed': True,
                    'primitive_id': 1,
                    'shader_out_col': fmt_adjusted(1, 1, 1, alpha_value),
                    'shader_out_depth': 0.33,
                    'post_mod_col': fmt_clamped(1, 1, 1, alpha_value),
                },
            ]
        self.check_events(events, modifs)

        if self.has_colour:
            x, y = self.relative_xy(120, 110)
            rdtest.log.print("Testing D3D no-output shader {}, {}".format(x, y))
            test_marker = self.find_action("No Output Shader", base_eid)
            self.controller.SetFrameEvent(test_marker.next.eventId, True)

            modifs = self.controller.PixelHistory(tex, x, y, sub, comp)
            events = [
                {
                    'event_name': 'Begin RenderPass',
                },
                {
                    'event_name': 'Unbound Shader',
                },
                {
                    'event_name': 'Stencil Write',
                },
                {
                    'event_name': 'No Output Shader',
                    'passed': True,
                    'unboundPS': True,
                    'primitive_id': 0
                },
            ]
            self.check_events(events, modifs)

    def check_final_colour(self, tex, x, y, modifs: List[rd.PixelModification], sub, comp):
        m = modifs[-1]
        if self.has_colour:
            expected = self.fetch_property['value'](m.postMod.col)
        else:
            expected = (m.postMod.depth, m.postMod.stencil/255.0, 0, 1)
        self.check_pixel_value(tex, x, y, expected, sub=sub, cast=comp)

    def check_events(self, events, modifs: List[rd.PixelModification]):
        # remove any modifs that didn't happen in the batch we're looking at -
        # targets can be reused between batches
        modifs = [
            m for m in modifs if self.batch_base.eventId in self.get_hierarchy(m.eventId)]

        if len(modifs) != len(events):
            rdtest.log.print(str([e['event_name'] for e in events]))
            rdtest.log.print(str([self.fetch_property['event_name'](m) for m in modifs]))
            self.error(f"Expected {len(events)} events got {len(modifs)}")
            return

        if not self.is_secondary:
            self.check_modifs_consistent(modifs)

            self.check_final_colour(self.tex, self.x, self.y,
                                    modifs, self.sub, self.comp)

        for i, m in enumerate(modifs):
            m = modifs[i]
            for prop_name, expected in events[i].items():
                prop_getter = self.fetch_property[prop_name]

                # property disabled, e.g. because colour or depth is not present, or we're in a secondary
                if prop_getter is None:
                    # must have a reason - don't allow tests to skip in the main check
                    self.check(
                        not self.has_colour or not self.has_depth or not self.has_stencil or self.is_secondary)
                    continue

                actual = prop_getter(m)

                epsilon = self.epsilon

                if prop_name == 'shader_out_depth':
                    # reasonable F32 epsilon close to 1.0
                    epsilon = 6.0e-8

                if not rdtest.value_compare(actual, expected, eps=epsilon):
                    self.error(
                        f"eventId {m.eventId}, primitiveID {m.primitiveID}: " +
                        f"testing {prop_name} on modification {i} expected {expected}, got {actual}")

    def check_modifs_consistent(self, modifs: List[rd.PixelModification]):
        # postmod of each should match premod of the next
        for i in range(len(modifs) - 1):
            m = modifs[i]
            n = modifs[i + 1]

            if not m.postMod.IsValid() or not n.preMod.IsValid():
                continue

            a = self.fetch_property['value'](m.postMod.col)
            b = self.fetch_property['value'](n.preMod.col)

            # A fragment event. If we have depth postMod.stencil should be unknown and depth should be consistent
            if m.eventId == n.eventId and self.has_depth:
                if self.has_stencil and not unknown_stencil(m.postMod.stencil):
                    self.error(
                        f"postmod stencil at EID {m.eventId} primitive {m.primitiveID}: {m.postMod.stencil} is not unknown")

                if not rdtest.value_compare(m.postMod.depth, n.preMod.depth):
                    self.error(
                        f"postmod depth at EID {m.eventId} primitive {m.primitiveID}: {m.postMod.depth} " +
                        f"doesn't match premod at next primitive {n.primitiveID}: {m.preMod.depth}")

            epsilon = self.epsilon

            if self.is_depth:
                a = (m.postMod.depth, m.postMod.stencil)
                b = (n.preMod.depth, n.preMod.stencil)

                epsilon = 1.0e-5

            if not rdtest.value_compare(a, b, eps=epsilon):
                self.error(
                    f"postmod at EID {m.eventId} primitive {m.primitiveID}: {a} "
                    f"doesn't match premod at {n.eventId} primitive {n.primitiveID}: {b}")

        # if we're missing either colour or depth, or it's a clear (which never is affected by the other even if it's bound),
        # check that modifications list that properly
        for m in modifs:
            if not self.has_colour or (self.get_actionflags(m.eventId) & rd.ActionFlags.ClearDepthStencil):
                if not rdtest.value_compare(m.preMod.col.uintValue, (0, 0, 0, 0)):
                    self.error(f"preMod is not zeroed for missing color at EID {m.eventId}")
                if not rdtest.value_compare(m.postMod.col.uintValue, (0, 0, 0, 0)):
                    self.error(f"postMod is not zeroed for missing color at EID {m.eventId}")
                if not rdtest.value_compare(m.shaderOut.col.uintValue, (0, 0, 0, 0)):
                    self.error(f"shaderOut is not zeroed for missing color at EID {m.eventId}")
            if not self.has_depth or (self.get_actionflags(m.eventId) & rd.ActionFlags.ClearColor):
                if not rdtest.value_compare(m.preMod.depth, -1.0):
                    self.error(f"preMod depth is not -1 for missing depth at EID {m.eventId}")
                if not rdtest.value_compare(m.preMod.stencil, -1):
                    self.error(f"preMod stencil is not -1 for missing depth at EID {m.eventId}")
                if not rdtest.value_compare(m.postMod.depth, -1.0):
                    self.error(f"postMod depth is not -1 for missing depth at EID {m.eventId}")
                if not rdtest.value_compare(m.postMod.stencil, -1):
                    self.error(f"postMod stencil is not -1 for missing depth at EID {m.eventId}")
                if not rdtest.value_compare(m.shaderOut.depth, -1.0):
                    self.error(f"shaderOut depth is not -1 for missing depth at EID {m.eventId}")
                if not rdtest.value_compare(m.shaderOut.stencil, -1):
                    self.error(f"shaderOut stencil is not -1 for missing depth at EID {m.eventId}")

        # Check that if the test failed, its postmod is the same as premod
        for i in range(len(modifs)):
            if not m.Passed() and m.preMod.IsValid() and m.postMod.IsValid():
                a = self.fetch_property['value'](m.preMod.col)
                b = self.fetch_property['value'](m.postMod.col)

                epsilon = self.epsilon

                if self.is_depth:
                    a = (m.preMod.depth, m.preMod.stencil)
                    b = (m.postMod.depth, m.postMod.stencil)

                    epsilon = 1.0e-5

                    if a[1] == -2 or b[1] == -2:
                        a = (a[0], -2)
                        b = (b[0], -2)

                if not rdtest.value_compare(a, b, eps=epsilon):
                    self.error(
                        f"postmod at EID {m.eventId} primitive {m.primitiveID}: {b} doesn't match premod: {a}")
