Compare commits

...
14 Commits
Author SHA1 Message Date
NotAlexNoyleandClaude Fable 5.1 db070f0003 addons: an add-on naming both the AppCompat GUID and the toolkit is matched by the entry that fits
matchingTargetApplication prefers, in order: this application's own id, the
AppCompat GUID (extensions.guid.appCompatId, the Firefox GUID), the toolkit.
The AppCompat entry was taken whenever present, whatever its version range.

An add-on can name both for different reasons: the GUID entry for the
application that owns that GUID, at that application's versions (Basilisk is
{ec8030f7-...} at 52.9.x), and a toolkit entry meaning "any other application
on this platform". For an application that is neither, with an AppCompat
version of 56.9, the GUID entry "52.9.0 - 52.9.*" under strict compatibility
does not fit - and because it was preferred, the toolkit entry that does
describe the application was never looked at. The add-on was installed and
immediately disabled as incompatible.

Now the AppCompat entry is used only if it admits the AppCompat version (both
bounds under strict compatibility, the lower bound otherwise - the rule
isCompatibleWith applies) or if there is no toolkit entry to fall back to.
Nothing changes for an add-on that names one of the two, or whose GUID entry
fits.

Found with Palefill 1.30 (toolkit@mozilla.org 4.8.2 - 10.*, platform 6.9.0),
which now installs enabled and applies its fixes.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LcrVQFRMC2ANGDfVaRG9Ed
2026-10-05 18:11:17 -04:00
NotAlexNoyleandClaude Fable 5.1 628ee50c99 plugin-container: do not hand the bundled library path to what the plugin runs
The plugin child is started through the bundled loader with an explicit
--library-path, which is what its own dlopen()s search; the LD_LIBRARY_PATH it
also inherits is not needed by it, but is inherited again by anything the
plugin spawns. Flash for Linux runs sh -c "ps x | grep netscape" at start-up:
the device's own /bin/sh (glibc 2.8) then loaded the bundled glibc 2.23 and
died with SIGSEGV, leaving a crash report for every page with Flash on it.

Unset it at child start-up, only when the bundled loader is in use
(MOZ_CHILD_PROCESS_LOADER), so a desktop build keeps its environment.

On the device: a Flash page loads and plays, and no report is written.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LcrVQFRMC2ANGDfVaRG9Ed
2026-10-05 15:46:38 -04:00
NotAlexNoyleandClaude Fable 5.1 0048d50fa9 plugins: an instance with no window to be given is windowless, not a null dereference
nsPluginInstanceOwner::CreateWidget() creates a child-window widget for a
windowed plugin and then calls Create() on it without checking that the widget
exists. A build with no child-window widget (the offscreen embedding registers
none for kWidgetCID) got null back and crashed with SIGSEGV on every page whose
Flash movie was embedded the ordinary way: a plugin is windowed unless it asks
otherwise, and Flash asks only for wmode=opaque or transparent.

With no widget to create, mark the instance windowless and carry on down the
windowless path. That is what webOS's own browser did - it had no plugin
windows and handed every plugin a drawable - and the webOS Flash build draws
through the Palm window structure in both modes.

Found on the device: two "Flash test" pages reached from a search results page
killed the daemon in a loop (the card reconnects and reopens the address).
After the change both render their movies and the daemon keeps its PID.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LcrVQFRMC2ANGDfVaRG9Ed
2026-10-05 15:30:05 -04:00
NotAlexNoyle 9dedcd5fe5 extensions: a link or navigation to an .xpi must reach the install flow in a chrome-less embedding
amContentHandler cancels the load and then hands the install to either a
chrome-privileged frameElement above the page or the content frame message
manager. This embedding has neither, so the hand-off threw after the load had
already been cancelled: following a plain link to an .xpi, or typing its
address, did nothing — no prompt, no error. InstallTrigger.install() was fixed
the same way earlier; call the add-ons integration service directly here too.
2026-10-01 13:22:49 -04:00
NotAlexNoyle 1fff802335 gfx: BasicPlanarYCbCrImage::CopyData must not report failure under delayed conversion
BasicImageLayer::Paint swaps the BasicLayerManager's image factory into the
video ImageContainer for the duration of a paint. A decoder thread that calls
CreatePlanarYCbCrImage() inside that window gets a BasicPlanarYCbCrImage, and
VideoData::SetVideoDataToImage always requests delayed conversion before
CopyData(). CopyData copied the planes and then returned false, so
CreateAndCopyData returned null and the decoder reported
NS_ERROR_OUT_OF_MEMORY -> MEDIA_ERR_DECODE.

With OMTC off a software paint on the TouchPad takes 40-160 ms, so a decode
landed in the window within the first 10-60 frames of every clip.

Return the base copy's result, and true when conversion is delayed: the
image is fully usable, GetAsSourceSurface() converts on demand.
2026-10-01 09:12:50 -04:00
NotAlexNoyleandClaude Opus 4.8 bd49435b11 cubeb: classify media streams + export a playing-stream count for the daemon
Set media.role=music by default (tunable/disable via $JIHAD_STATE_DIR/pulse.props) so the
stream takes the webOS media-policy path (pmedia), and maintain an exported
jihad_pulse_active_streams() count (visibility(default), like MOZ_EXPORT) that the daemon polls
to hold the audiod media scenario + force the speaker route while audio plays. Per-stream flag
inc/dec on play/stop/destroy; calloc zero-inits it. UNVERIFIED by ear — see impl-audio-backend.md.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-08-17 10:15:19 -04:00
NotAlexNoyleandClaude Opus 4.8 81e10aa37e videocontrols: surface media errors for audio-only content
setupStatusFader hid the status overlay for isAudioOnly unconditionally, which also hid
the ERROR state — so an <audio> whose format cannot be decoded (MP3/AAC have no decoder
in this build; err=4 MEDIA_ERR_SRC_NOT_SUPPORTED) rendered a live-looking control bar with
no failure shown (the forbidden works-looking-but-silent shape, gre-widgets R1). Keep the
throbber/loading suppression but let a genuine, non-suppressed video.error surface for
audio-only too. Normal decodable audio is untouched. Device-verified: MP3 <audio> now shows
the X + 'format is not supported' overlay.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-08-17 08:12:56 -04:00
NotAlexNoyleandClaude Opus 4.8 0c10df5c94 videocontrols: fix scrubber flicker/settle + interpolate the thumb
On the offscreen webOS surface the media-controls scrubber had three defects, all fixed
here (the flicker root cause was the daemon's per-buffer damage, fixed separately):

- showDuration ran every timeupdate, re-setting scrubber.max (relayout) and the thumb's
  showhours attr (restyle) each tick -> jitter + a flashing time bubble. Dedup on the
  finite duration so it runs once.
- 'ended' snapped the thumb to the throttled currentTime, a few ms short of the end, so it
  never came to rest at the clip end. Snap to duration for finite durations.
- timeupdate fires ~4x/sec, so the thumb stepped forward in visible jumps. A 40ms interval
  (25fps) drives the thumb position from the live currentTime clock while playing. 40ms is
  the ceiling: 25ms/40fps overruns the adapter's composite-and-return round-trip and stalls
  (checked against Atlas BrowserServer, which uses the same double-buffer + wait-for-return
  flow control with fewer buffers). Only nudges scrubber.value; duration stays owned by the
  event path; guarded on a known finite duration so it never runs before metadata.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-08-16 10:05:39 -04:00
NotAlexNoyleandClaude Opus 4.8 07259a27f0 Jihad engine mods: NPAPI/webOS plugin host, offscreen PuppetWidget, headless toolkit, pulse cubeb, PmLog sem shim
Vendored delta on pristine UXP b2594a4 (upstream master). Captures the full
jihad engine patch set as a durable commit so the OE bitbake clone builds the
exact source the desktop host build compiles, instead of a stale patch queue
that no longer applies to pristine UXP.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-08-16 08:35:50 -04:00
Moonchild b2594a4ace Merge pull request 'Revert gtk file dialog change and set default to cancel' (#3137) from 3136 into master
Reviewed-on: https://repo.palemoon.org/MoonchildProductions/UXP/pulls/3137
2026-06-30 13:54:19 +00:00
Moonchild 30e29845c4 Merge pull request 'Decouple NPAPI from GTK2' (#3133) from Basilisk-Dev/UXP-contrib:npapi-no-gtk2 into master
Reviewed-on: https://repo.palemoon.org/MoonchildProductions/UXP/pulls/3133
2026-06-30 13:51:11 +00:00
Basilisk-Dev d8662c5dd9 Issue #3132 - use ENABLE check for npapi gtk2 builds instead of a dedicated DISABLE check to match other config options 2026-06-30 09:15:45 -04:00
Basilisk-Dev 0a5e50a9fe Issue #3132 - Resolve GTK3 ATK bridge dynamically 2026-06-26 13:49:15 -04:00
Basilisk-Dev 3f5b7ed020 Issue #3132 - Add GTK2 NPAPI build toggle 2026-06-26 13:09:49 -04:00
78 changed files with 5086 additions and 153 deletions

No files matched your search

+48 -7
View File
@@ -18,10 +18,6 @@
#endif
#include <gtk/gtk.h>
#if (MOZ_WIDGET_GTK == 3)
extern "C" __attribute__((weak,visibility("default"))) int atk_bridge_adaptor_init(int*, char **[]);
#endif
using namespace mozilla;
using namespace mozilla::a11y;
@@ -33,12 +29,20 @@ extern "C" {
typedef GType (* AtkGetTypeType) (void);
typedef void (*GnomeAccessibilityInit) (void);
typedef void (*GnomeAccessibilityShutdown) (void);
#if (MOZ_WIDGET_GTK == 3)
typedef int (*AtkBridgeAdaptorInit) (int*, char***);
#endif
}
static PRLibrary* sATKLib = nullptr;
static const char sATKLibName[] = "libatk-1.0.so.0";
static const char sATKHyperlinkImplGetTypeSymbol[] =
"atk_hyperlink_impl_get_type";
#if (MOZ_WIDGET_GTK == 3)
static PRLibrary* sAtkBridgeAdaptorLib = nullptr;
static const char sAtkBridgeAdaptorLibName[] = "libatk-bridge-2.0.so.0";
static const char sAtkBridgeAdaptorInitSymbol[] = "atk_bridge_adaptor_init";
#endif
gboolean toplevel_event_watcher(GSignalInvocationHint*, guint, const GValue*,
gpointer);
@@ -127,6 +131,38 @@ LoadGtkModule(GnomeAccessibilityModule& aModule)
return NS_OK;
}
#if (MOZ_WIDGET_GTK == 3)
static bool
InitAtkBridgeAdaptor()
{
AtkBridgeAdaptorInit initBridge =
reinterpret_cast<AtkBridgeAdaptorInit>(
PR_FindFunctionSymbolAndLibrary(sAtkBridgeAdaptorInitSymbol,
&sAtkBridgeAdaptorLib));
if (!initBridge) {
sAtkBridgeAdaptorLib = PR_LoadLibrary(sAtkBridgeAdaptorLibName);
if (sAtkBridgeAdaptorLib) {
initBridge =
reinterpret_cast<AtkBridgeAdaptorInit>(
PR_FindFunctionSymbol(sAtkBridgeAdaptorLib,
sAtkBridgeAdaptorInitSymbol));
if (!initBridge) {
PR_UnloadLibrary(sAtkBridgeAdaptorLib);
sAtkBridgeAdaptorLib = nullptr;
}
}
}
if (!initBridge) {
return false;
}
initBridge(nullptr, nullptr);
return true;
}
#endif
void
a11y::PlatformInit()
{
@@ -181,9 +217,7 @@ a11y::PlatformInit()
// Init atk-bridge now
PR_SetEnv("NO_AT_BRIDGE=0");
#if (MOZ_WIDGET_GTK == 3)
if (atk_bridge_adaptor_init) {
atk_bridge_adaptor_init(nullptr, nullptr);
} else
if (!InitAtkBridgeAdaptor())
#endif
{
nsresult rv = LoadGtkModule(sAtkBridge);
@@ -228,6 +262,13 @@ a11y::PlatformShutdown()
sAtkBridge.init = nullptr;
sAtkBridge.shutdown = nullptr;
}
#if (MOZ_WIDGET_GTK == 3)
if (sAtkBridgeAdaptorLib) {
// Do not shutdown/unload atk-bridge; an exit function registered by
// the bridge will take care of it.
sAtkBridgeAdaptorLib = nullptr;
}
#endif
#if (MOZ_WIDGET_GTK == 2)
if (sGail.lib) {
// Do not shutdown gail because
+1
View File
@@ -164,6 +164,7 @@ def old_configure_options(*options):
'--enable-negotiateauth',
'--enable-nfc',
'--enable-npapi',
'--enable-npapi-gtk2',
'--enable-nspr-build',
'--enable-official-branding',
'--enable-official-vendor',
+5
View File
@@ -88,6 +88,11 @@ check_and_add_gcc_warning('-Wno-error=deprecated-declarations')
# false positives depending on optimization
check_and_add_gcc_warning('-Wno-error=array-bounds')
# GCC 9+ value-range analysis trips -Wformat-overflow on null %s args in debug
# spew (e.g. js/src/jit/x64/BaseAssembler-x64.h:595); harmless for our build,
# and js/src forces -Werror=format so this must be downgraded here to reach it.
check_and_add_gcc_warning('-Wno-error=format-overflow')
# can't get rid of those PGO warnings
check_and_add_gcc_warning('-Wno-error=coverage-mismatch', when='MOZ_PGO')
+1 -1
View File
@@ -52,7 +52,7 @@ static RedirEntry kRedirMap[] = {
{ "config", "chrome://global/content/config.xul", 0 },
{ "console", "chrome://global/content/console.xul", 0 },
{
"credits", "http://www.palemoon.org/Contributors.shtml",
"credits", "https://net.riverstonerelay.jihad-browser/credits",
nsIAboutModule::URI_SAFE_FOR_UNTRUSTED_CONTENT |
nsIAboutModule::MAKE_LINKABLE
},
+28
View File
@@ -199,6 +199,7 @@
#include "nsICommandManager.h"
#include "nsIDOMNode.h"
#include "nsIDocShellTreeOwner.h"
#include "nsIBadCertListener2.h" // JIHAD: route cert errors to the embedder chrome
#include "nsIHttpChannel.h"
#include "nsIIDNService.h"
#include "nsIInputStreamChannel.h"
@@ -1074,6 +1075,33 @@ nsDocShell::GetInterface(const nsIID& aIID, void** aSink)
} else if (aIID.Equals(NS_GET_IID(nsITabChild))) {
*aSink = GetTabChild().take();
return *aSink ? NS_OK : NS_ERROR_FAILURE;
} else if (aIID.Equals(NS_GET_IID(nsIBadCertListener2))) {
// JIHAD: hand the embedder's chrome the certificate error.
//
// NSS asks the channel's notification callbacks for nsIBadCertListener2
// (SSLServerCertVerification.cpp, CertErrorRunnable::CheckCertOverrides) and passes
// it the nsISSLStatus carrying the SERVER CERTIFICATE. That call is the one and only
// place an embedder is handed the cert while it still exists — the same shape the
// Atlas browser gets for free from WebKit's load-failed-with-tls-errors signal, which
// delivers the GTlsCertificate as a signal argument.
//
// Without this hop nothing answers: the docShell's fallthrough is
// nsDocLoader::GetInterface, which Queries the docloader ITSELF, so the request never
// reaches nsIWebBrowserChrome. The embedder is then left trying to recover the cert
// afterwards, and there is no way to: the failed channel's securityInfo has a null
// SSLStatus, and UXP has no nsIRecentBadCerts service. Measured 2026-08-04 — the
// SSL-confirm dialog worked, the user accepted, and the accept could do nothing
// because RememberValidityOverride had no nsIX509Cert to take.
//
// Routed through the tree owner, exactly as nsIWebBrowserChromeFocus and nsIPrompt
// already are (nsDocShellTreeOwner::GetInterface).
nsCOMPtr<nsIDocShellTreeOwner> treeOwner;
GetTreeOwner(getter_AddRefs(treeOwner));
nsCOMPtr<nsIInterfaceRequestor> req(do_QueryInterface(treeOwner));
if (req) {
return req->GetInterface(aIID, aSink);
}
return NS_NOINTERFACE;
} else if (aIID.Equals(NS_GET_IID(nsIContentFrameMessageManager))) {
nsCOMPtr<nsITabChild> tabChild =
do_GetInterface(static_cast<nsIDocShell*>(this));
+33 -5
View File
@@ -787,6 +787,12 @@ nsresult
nsObjectLoadingContent::InstantiatePluginInstance(bool aIsLoading)
{
#ifdef MOZ_ENABLE_NPAPI
// JIHAD DIAGNOSTIC (R7): entry + the four early-out conditions. If this line never appears the
// plugin was never asked for at all (no nsPluginFrame -> no HasNewFrame), which is a different
// bug from being asked for and refused.
fprintf(stderr, "[jihad-npapi] InstantiatePluginInstance(aIsLoading=%d) owner=%d type=%d "
"mIsLoading=%d instantiating=%d\n", (int)aIsLoading, (int)!!mInstanceOwner,
(int)mType, (int)mIsLoading, (int)mInstantiating);
if (mInstanceOwner || mType != eType_Plugin || (mIsLoading != aIsLoading) ||
mInstantiating) {
// If we hit this assertion it's probably because LoadObject re-entered :(
@@ -822,10 +828,12 @@ nsObjectLoadingContent::InstantiatePluginInstance(bool aIsLoading)
NS_ENSURE_TRUE(mInstantiating, NS_OK);
if (!thisContent->GetPrimaryFrame()) {
// JIHAD DIAGNOSTIC (R7): a silent NS_OK return — the single most likely place for a plugin
// to "do nothing" with no error anywhere.
fprintf(stderr, "[jihad-npapi] NO PRIMARY FRAME -> not instantiating\n");
LOG(("OBJLC [%p]: Not instantiating plugin with no frame", this));
return NS_OK;
}
nsresult rv = NS_ERROR_FAILURE;
RefPtr<nsPluginHost> pluginHost = nsPluginHost::GetInst();
@@ -846,6 +854,8 @@ nsObjectLoadingContent::InstantiatePluginInstance(bool aIsLoading)
rv = pluginHost->InstantiatePluginInstance(mContentType,
mURI.get(), this,
getter_AddRefs(newOwner));
fprintf(stderr, "[jihad-npapi] pluginHost->InstantiatePluginInstance rv=0x%x owner=%d\n",
(unsigned)rv, (int)!!newOwner);
// XXX(johns): We don't suspend native inside stopping plugins...
if (appShell) {
@@ -2378,6 +2388,11 @@ nsObjectLoadingContent::LoadObject(bool aNotify,
FallbackType clickToPlayReason;
if (!mActivated && (mType == eType_Null || mType == eType_Plugin) &&
!ShouldPlay(clickToPlayReason, false)) {
// JIHAD DIAGNOSTIC (R7): this is where a plugin that resolved fine is silently demoted to
// eType_Null. Every path out of ShouldPlay that returns false lands here, so one line here
// names the reason instead of instrumenting each exit.
fprintf(stderr, "[jihad-npapi] demoted to fallback: mType=%d fallbackReason=%d\n",
(int)mType, (int)clickToPlayReason);
LOG(("OBJLC [%p]: Marking plugin as click-to-play", this));
mType = eType_Null;
fallbackType = clickToPlayReason;
@@ -2887,9 +2902,16 @@ nsObjectLoadingContent::GetTypeOfContent(const nsCString& aMIMEType)
#ifdef MOZ_ENABLE_NPAPI
RefPtr<nsPluginHost> pluginHost = nsPluginHost::GetInst();
if ((caps & eSupportPlugins) &&
pluginHost &&
pluginHost->HavePluginForType(aMIMEType, nsPluginHost::eExcludeNone)) {
bool jihadHave = pluginHost &&
pluginHost->HavePluginForType(aMIMEType, nsPluginHost::eExcludeNone);
// JIHAD DIAGNOSTIC (R7 windowless port): says, for one MIME type, whether the element
// advertised plugin capability and whether the host has a plugin — the two facts that decide
// if the NPAPI subsystem is entered at all. Added because a plugin that never instantiates
// produces NO plugin logging whatsoever, which is indistinguishable from one that was never
// asked for.
fprintf(stderr, "[jihad-npapi] GetTypeOfContent mime=%s caps=0x%x supportsPlugins=%d have=%d\n",
nsCString(aMIMEType).get(), caps, (int)!!(caps & eSupportPlugins), (int)jihadHave);
if ((caps & eSupportPlugins) && jihadHave) {
// ShouldPlay will handle checking for disabled plugins
return eType_Plugin;
}
@@ -3486,9 +3508,15 @@ nsObjectLoadingContent::ShouldPlay(FallbackType &aReason, bool aIgnoreCurrentTyp
aReason = eFallbackClickToPlay;
uint32_t enabledState = nsIPluginTag::STATE_DISABLED;
pluginHost->GetStateForType(mContentType, nsPluginHost::eExcludeNone,
nsresult jihadStateRv = pluginHost->GetStateForType(mContentType, nsPluginHost::eExcludeNone,
&enabledState);
// JIHAD DIAGNOSTIC (R7): note that enabledState is pre-set to STATE_DISABLED and the rv of
// GetStateForType is NOT checked upstream, so a failing lookup silently blocks the plugin
// with no other trace. Log the rv as well as the value.
fprintf(stderr, "[jihad-npapi] ShouldPlay type=%s enabledState=%u rv=0x%x\n",
mContentType.get(), enabledState, (unsigned)jihadStateRv);
if (nsIPluginTag::STATE_DISABLED == enabledState) {
fprintf(stderr, "[jihad-npapi] ShouldPlay -> FALSE (eFallbackDisabled)\n");
aReason = eFallbackDisabled;
return false;
}
+160
View File
@@ -577,6 +577,166 @@ typedef struct _NPEvent
} NPEvent;
#elif defined(XP_UNIX) && defined(MOZ_X11)
typedef XEvent NPEvent;
#elif defined(XP_UNIX) && defined(MOZ_WIDGET_HEADLESS)
/*
* JIHAD (webOS, cavekit-addons-extensions.md R7) — THE PALM EVENT PROTOCOL.
*
* webOS plugins do not speak X11. Palm's own npapi.h defines NPEvent, under XP_WEBOS, as the
* union below; the device's Adobe Flash was compiled against exactly that header and its
* NPP_HandleEvent reads these fields. Reproduced here from the webOS WebKit fork's
* WebCore/plugins/npapi.h (the XP_WEBOS block, NpPalmEventsEnum through the NPEvent union),
* so that the shape crossing our NPAPI boundary is the shape the plugin was built to read.
*
* This replaces `typedef void* NPEvent` for the headless toolkit, and that arm was not merely
* a placeholder — it was load-bearing in the wrong direction. NPRemoteEvent is a raw memcpy of
* sizeof(NPEvent) (dom/plugins/ipc/NPEventUnix.h), so with a void* NPEvent the parent shipped
* four bytes of its own address space to the child and the child handed the plugin a pointer
* into a foreign process. No event could ever have carried a payload. Making NPEvent a real
* POD is what lets the whole existing NPP_HandleEvent path work unmodified, parent to child.
*
* Gated on MOZ_WIDGET_HEADLESS because that is precisely the configuration this port builds
* (--enable-default-toolkit=cairo-headless, no X anywhere in the bundle) and the only one that
* reaches this arm. A non-webOS headless embedding would get these structs too; that is
* harmless, since the alternative it loses is an NPEvent that could not carry an event.
*
* POINTER-BEARING MEMBERS DO NOT SURVIVE IPC. dstBuffer/graphicsContext (draw),
* touches (touch) and reserved (system) are process-local. Pen, key and system events use
* none of them; the draw event is built inside the child (PluginInstanceChild.cpp) and never
* crosses the boundary, which is why it stays valid.
*
* One deliberate omission from Palm's union: NpPalmAccelerometerEvent. It is 16 bytes against
* NpPalmDrawEvent's 44, so leaving it out changes sizeof(NPEvent) by nothing — and the union
* is only ever read through the plugin's OWN copy of this declaration, so an unused member
* here is unobservable. It is left out rather than copied because it carries a struct-scoped
* `enum Orientation` whose tag would leak to file scope, for no gain.
*/
typedef enum NpPalmEventsEnum
{
npPalmPenDownEvent = 1 << 0,
npPalmPenUpEvent = 1 << 1,
npPalmPenMoveEvent = 1 << 2,
npPalmKeyDownEvent = 1 << 3,
npPalmKeyUpEvent = 1 << 4,
npPalmKeyRepeatEvent = 1 << 5,
npPalmKeyPressEvent = 1 << 6,
npPalmDrawEvent = 1 << 7,
npPalmSystemEvent = 1 << 8,
npPalmGestureEvent = 1 << 9,
npPalmAccelerometerEvent = 1 << 10,
npPalmTouchStartEvent = 1 << 11,
npPalmTouchMoveEvent = 1 << 12,
npPalmTouchEndEvent = 1 << 13,
npPalmTouchCancelledEvent = 1 << 14,
npPalmPenDoubleClickEvent = 1 << 15,
npPalmPenClickEvent = 1 << 16
} NpPalmEventsEnum;
typedef enum NpPalmSystemEventsEnum
{
npPalmPauseEvent = 1,
npPalmResumeEvent = 2,
npPalmGainFocusEvent = 3,
npPalmLoseFocusEvent = 4,
npPalmDimmedEvent = 5,
npPalmSetFullScreenEvent = 6,
npPalmUnsetFullScreenEvent = 7,
npPalmPageLoadingEvent = 8,
npPalmPageLoadCompleteEvent = 9,
npPalmViewPortChangedEvent = 10,
npPalmSpotlightStartEvent = 11,
npPalmSpotlightEndEvent = 12
} NpPalmSystemEventsEnum;
typedef enum NpPalmKeyModifiersEnum
{
npPalmCtrlKeyModifier = 1 << 0,
npPalmAltKeyModifier = 1 << 1,
npPalmShiftKeyModifier = 1 << 2,
npPalmMetaKeyModifier = 1 << 3
} NpPalmKeyModifiersEnum;
typedef struct NpPalmKeyEvent
{
int32_t chr; /* 32-bit so any Unicode character fits */
int32_t modifiers;
int32_t rawkeyCode;
int32_t rawModifier;
} NpPalmKeyEvent;
typedef struct NpPalmPenEvent
{
int32_t xCoord, yCoord; /* plugin-local, in the plugin's own unscaled pixels */
int32_t modifiers;
} NpPalmPenEvent;
typedef struct NpPalmTouchPoint
{
int32_t xCoord, yCoord;
} NpPalmTouchPoint;
typedef struct NpPalmTouchEvent
{
int32_t touchCount;
NpPalmTouchPoint* touches; /* process-local — never send over IPC */
int32_t modifiers;
} NpPalmTouchEvent;
typedef struct NpPalmDrawEvent
{
void* dstBuffer; /* process-local; API 1.0 raster destination */
uint32_t dstRowBytes;
int32_t srcLeft;
int32_t srcRight;
int32_t srcTop;
int32_t srcBottom;
void* graphicsContext; /* process-local; the Piranha PGContext, API 2.0 */
int32_t dstLeft;
int32_t dstRight;
int32_t dstTop;
int32_t dstBottom;
} NpPalmDrawEvent;
typedef struct NpPalmSystemEvent
{
NpPalmSystemEventsEnum type;
int32_t value;
int32_t viewLeft;
int32_t viewRight;
int32_t viewTop;
int32_t viewBottom;
void* reserved; /* process-local */
} NpPalmSystemEvent;
typedef enum NpPalmGestureEventEnum
{
npPalmGestureStartEvent = 1,
npPalmGestureChangeEvent = 2,
npPalmGestureEndEvent = 3,
npPalmGestureSingleTapEvent = 4
} NpPalmGestureEventEnum;
typedef struct NpPalmGestureEvent
{
NpPalmGestureEventEnum type;
int32_t x, y;
float scale, rotate;
int32_t center_x, center_y;
int32_t modifiers;
} NpPalmGestureEvent;
typedef struct NPEvent
{
NpPalmEventsEnum eventType;
union {
NpPalmKeyEvent keyEvent;
NpPalmPenEvent penEvent;
NpPalmDrawEvent drawEvent;
NpPalmSystemEvent systemEvent;
NpPalmGestureEvent gestureEvent;
NpPalmTouchEvent touchEvent;
} data;
} NPEvent;
#else
typedef void* NPEvent;
#endif
+15 -1
View File
@@ -341,6 +341,10 @@ nsNPAPIPlugin::CreatePlugin(nsPluginTag *aPluginTag, nsNPAPIPlugin** aResult)
RefPtr<nsNPAPIPlugin> plugin = new nsNPAPIPlugin();
PluginLibrary* pluginLib = GetNewPluginLibrary(aPluginTag);
// JIHAD DIAGNOSTIC (R7): GetNewPluginLibrary is where the OOP decision is made —
// RunPluginOOP() ? PluginModuleParent::LoadModule (launches plugin-container) : a
// PluginPRLibrary loaded in-process. A null here is the plugin-container launch failing.
fprintf(stderr, "[jihad-npapi] GetNewPluginLibrary lib=%d\n", (int)!!pluginLib);
if (!pluginLib) {
return NS_ERROR_FAILURE;
}
@@ -386,6 +390,7 @@ nsNPAPIPlugin::CreatePlugin(nsPluginTag *aPluginTag, nsNPAPIPlugin** aResult)
#else
NPError pluginCallError;
nsresult rv = pluginLib->NP_Initialize(&sBrowserFuncs, &plugin->mPluginFuncs, &pluginCallError);
fprintf(stderr, "[jihad-npapi] NP_Initialize rv=0x%x npErr=%d\n", (unsigned)rv, (int)pluginCallError);
if (rv != NS_OK || pluginCallError != NPERR_NO_ERROR) {
return NS_ERROR_FAILURE;
}
@@ -1939,7 +1944,16 @@ _getvalue(NPP npp, NPNVariable variable, void *result)
}
case NPNVSupportsWindowless: {
#if defined(XP_WIN) || defined(XP_MACOSX) || \
// MOZ_WIDGET_HEADLESS added for Jihad Browser. This list enumerates the toolkits that can
// host a windowless plugin, and it was written before a headless toolkit existed — so a
// cairo-headless build answered "windowless unsupported" to every plugin, which is the
// opposite of the truth. Headless is the toolkit that MOST needs windowless: there is no X
// server, no Drawable and no XEmbed, so windowed hosting is impossible and drawing into a
// memory bitmap we own is the only thing that can work. gfxPlatformHeadless already returns
// true from SupportsPluginDirectBitmapDrawing(), i.e. the drawing model this unlocks
// (NPDrawingModelAsyncBitmapSurface) is implemented for this platform; only this
// advertisement was missing, and with it false no plugin would ever choose windowless.
#if defined(XP_WIN) || defined(XP_MACOSX) || defined(MOZ_WIDGET_HEADLESS) || \
(defined(MOZ_X11) && defined(MOZ_WIDGET_GTK))
*(NPBool*)result = true;
#else
+69 -1
View File
@@ -843,6 +843,9 @@ nsPluginHost::InstantiatePluginInstance(const nsACString& aMimeType, nsIURI* aUR
}
rv = SetUpPluginInstance(aMimeType, aURL, instanceOwner);
// JIHAD DIAGNOSTIC (R7): the real rv is flattened to NS_ERROR_FAILURE below, which is what
// made this failure opaque from the caller's side.
fprintf(stderr, "[jihad-npapi] SetUpPluginInstance rv=0x%x\n", (unsigned)rv);
if (NS_FAILED(rv)) {
instanceOwner->Destroy();
return NS_ERROR_FAILURE;
@@ -961,7 +964,12 @@ nsPluginHost::TrySetUpPluginInstance(const nsACString &aMimeType,
#endif
RefPtr<nsNPAPIPlugin> plugin;
GetPlugin(aMimeType, getter_AddRefs(plugin));
nsresult jihadGetRv = GetPlugin(aMimeType, getter_AddRefs(plugin));
// JIHAD DIAGNOSTIC (R7): GetPlugin -> CreateNPAPIPlugin -> nsNPAPIPlugin::CreatePlugin, which
// for an OOP build is PluginModuleParent::LoadModule (the plugin-container launch). Its rv is
// DISCARDED here; only the null check survives, so a launch failure is invisible.
fprintf(stderr, "[jihad-npapi] GetPlugin rv=0x%x plugin=%d\n",
(unsigned)jihadGetRv, (int)!!plugin);
if (!plugin) {
return NS_ERROR_FAILURE;
}
@@ -987,6 +995,7 @@ nsPluginHost::TrySetUpPluginInstance(const nsACString &aMimeType,
// except in some cases not Java, see bug 140931
// our COM pointer will free the peer
nsresult rv = instance->Initialize(plugin.get(), aOwner, aMimeType);
fprintf(stderr, "[jihad-npapi] instance->Initialize rv=0x%x\n", (unsigned)rv);
if (NS_FAILED(rv)) {
mInstances.RemoveElement(instance.get());
aOwner->SetInstance(nullptr);
@@ -4201,3 +4210,62 @@ PluginDestructionGuard::DelayDestroy(nsNPAPIPluginInstance *aInstance)
return false;
}
// ── JIHAD (webOS, cavekit-addons-extensions.md R7) ────────────────────────────────────────
//
// A C entry point for the render daemon, following the jihad_offscreen_* precedent in
// widget/PuppetWidget.cpp: the daemon links against libxul but cannot see internal classes,
// and nsNPAPIPluginInstance::HandleEvent is neither exported nor virtual, so there is no
// other way to reach a plugin from outside.
//
// This exists for the frozen YAP command pair asyncCmdPluginSpotlightStart/End. The card
// scrims and smart-zooms to a plugin, then tells the server the rect it settled on; the
// server's job is to pass that on to the plugin as Palm's npPalmSpotlightStartEvent(11) /
// npPalmSpotlightEndEvent(12). It is not cosmetic: Flash routes 11 to the same handler as
// npPalmSetFullScreenEvent(6) and 12 to the same as npPalmUnsetFullScreenEvent(7)
// (libflashplayer.so jump table at 0x44e0c — entries 10 and 11 target 0x44fa0/0x44e80,
// identical to entries 5 and 6), so this is how a plugin is told it went fullscreen.
//
// Broadcast to every running instance rather than targeting one: the spotlight is a
// page-level, one-plugin-at-a-time state, this daemon serves a single card, and the
// alternative — matching the rect against each instance's frame — would invent a
// disambiguation rule that the contract does not define. Returns how many instances were
// told, so a caller can tell "no plugin" from "delivered".
extern "C" MOZ_EXPORT int
jihad_plugin_palm_spotlight(int aOn, int aLeft, int aTop, int aRight, int aBottom)
{
#ifdef MOZ_ENABLE_NPAPI
RefPtr<nsPluginHost> host = nsPluginHost::GetInst();
if (!host) {
return 0;
}
NPEvent ev;
memset(&ev, 0, sizeof(ev));
ev.eventType = npPalmSystemEvent;
ev.data.systemEvent.type = aOn ? npPalmSpotlightStartEvent : npPalmSpotlightEndEvent;
// Palm's host leaves systemEvent.value uninitialised here and fills only the rect
// (libWebKitLuna handlePluginSpotlightStart 0x4e6cec-0x4e6d24, which never writes [sp+8]).
// The memset above zeroes it, which is strictly tidier and observably the same.
ev.data.systemEvent.viewLeft = aLeft;
ev.data.systemEvent.viewTop = aTop;
ev.data.systemEvent.viewRight = aRight;
ev.data.systemEvent.viewBottom = aBottom;
int delivered = 0;
// Copy the instance list first: HandleEvent re-enters the plugin, which may run script and
// tear an instance down underneath us.
nsTArray<RefPtr<nsNPAPIPluginInstance>> instances(host->JihadRunningInstances());
for (uint32_t i = 0; i < instances.Length(); ++i) {
int16_t response = kNPEventNotHandled;
if (NS_SUCCEEDED(instances[i]->HandleEvent(&ev, &response,
NS_PLUGIN_CALL_SAFE_TO_REENTER_GECKO))) {
delivered++;
}
}
return delivered;
#else
(void)aOn; (void)aLeft; (void)aTop; (void)aRight; (void)aBottom;
return 0;
#endif
}
+7
View File
@@ -274,6 +274,13 @@ public:
InfallibleTArray<nsCString>& result,
bool firstMatchOnly);
// Jihad R7 (webOS): a snapshot of the live instances, for jihad_plugin_palm_spotlight.
// A COPY, not a reference: dispatching an event re-enters the plugin, which can run script
// and destroy an instance, and mInstances is mutated by that teardown.
const nsTArray<RefPtr<nsNPAPIPluginInstance>>& JihadRunningInstances() const {
return mInstances;
}
private:
friend class nsPluginUnloadRunnable;
+216 -14
View File
@@ -569,6 +569,75 @@ NS_IMETHODIMP nsPluginInstanceOwner::InvalidateRect(NPRect *invalidRect)
GetContentsScaleFactor(&scaleFactor);
rect.ScaleRoundOut(scaleFactor);
mPluginFrame->InvalidateLayer(nsDisplayItem::TYPE_PLUGIN, &rect);
// JIHAD (2026-08-09): drive the offscreen daemon's re-capture for WINDOWLESS plugins.
//
// A windowless plugin has no mWidget, so the mWidget->Invalidate() branch above (which is what
// ultimately reaches PuppetWidget::Invalidate and sets the daemon's mJihadDirty flag) never
// runs. InvalidateLayer() alone only marks the plugin's ImageLayer for recomposition; with the
// in-process BasicLayerManager and no separate compositor, that repaints the widget's own layer
// backbuffer but never triggers jihad_offscreen_render_region, the path that actually renders a
// frame into the adapter's shared buffer for the card. Measured: Flash pushes a fresh magenta
// frame into its ImageContainer ~15x/s and BasicImageLayer::Paint runs on each, yet
// render_region fired ONCE (at load) so the card froze on the pre-content frame. The control
// plugin only appeared to work because its static fill was already present at that single
// capture. Invalidate the nearest real widget here so every async plugin frame marks the daemon
// dirty and gets composited to the card.
//
// THE RECT MATTERS, and the first version of this got it wrong. It invalidated
// nearest->GetBounds() — the WIDGET's bounds, i.e. the whole viewport — on the reasoning
// that the daemon re-renders the whole document per capture anyway. That reasoning stopped
// being true when the daemon learned to repaint only the damaged rows: a whole-viewport
// software render costs ~40 ms on this hardware against ~4 ms for a 320x240 plugin box, and
// a full-viewport damage box silently forced the expensive path on every single frame.
// Measured: an animated SWF ran at 19 fps with 1 partial repaint in 39, and the damage box
// logged as 768x942 with the plugin's own 320x240 unioned uselessly into it.
//
// So invalidate the plugin's own area, translated from plugin-local into the widget's
// coordinates the same way the rest of this file does it: content-box origin relative to the
// frame, offset to the widget, in device px.
if (!mWidget) {
nsIWidget* nearest = mPluginFrame->GetNearestWidget();
if (nearest) {
nsIntRect widgetRect = rect;
nsPresContext* pc = mPluginFrame->PresContext();
// Offset to the ROOT frame, which is what the widget's coordinates are relative to.
// GetReferenceFrame() was the first attempt and it is wrong here: for a plugin it
// resolves to a frame that makes the offset come out 0, so every invalidation landed at
// the viewport's top-left corner instead of on the plugin. That is not a small error —
// the damage-only repaint then faithfully refreshed the wrong 320x240 box and the
// plugin's own area never updated again, which reads on screen as the animation
// freezing after its first frame. This is the same offset nsPluginFrame itself uses to
// build NPWindow.x/y.
nsIFrame* rootFrame = nullptr;
if (nsIPresShell* ps = mPluginFrame->PresContext()->GetPresShell()) {
rootFrame = ps->GetRootFrame();
}
nsPoint origin = mPluginFrame->GetContentRectRelativeToSelf().TopLeft() +
(rootFrame ? mPluginFrame->GetOffsetToCrossDoc(rootFrame) : nsPoint());
widgetRect.MoveBy(pc->AppUnitsToDevPixels(origin.x),
pc->AppUnitsToDevPixels(origin.y));
// A plugin is entitled to report an invalid rect larger than itself, and an empty one
// would invalidate nothing at all; clamp to the plugin's own box, and fall back to the
// widget bounds if that leaves nothing (correctness before speed).
nsIntRect pluginBox(pc->AppUnitsToDevPixels(origin.x),
pc->AppUnitsToDevPixels(origin.y),
pc->AppUnitsToDevPixels(
mPluginFrame->GetContentRectRelativeToSelf().width),
pc->AppUnitsToDevPixels(
mPluginFrame->GetContentRectRelativeToSelf().height));
widgetRect = widgetRect.Intersect(pluginBox);
if (widgetRect.IsEmpty()) {
widgetRect = pluginBox;
}
if (widgetRect.IsEmpty()) {
nearest->Invalidate(nearest->GetBounds());
} else {
nearest->Invalidate(LayoutDeviceIntRect(widgetRect.x, widgetRect.y,
widgetRect.width, widgetRect.height));
}
}
}
return NS_OK;
}
@@ -2530,6 +2599,122 @@ nsEventStatus nsPluginInstanceOwner::ProcessEvent(const WidgetGUIEvent& anEvent)
rv = nsEventStatus_eConsumeNoDefault;
#endif
// Guard MUST match the one npapi.h uses to define NPEvent as Palm's union. Any other
// predicate compiles this against `typedef void* NPEvent` and does not build.
#if defined(XP_UNIX) && defined(MOZ_WIDGET_HEADLESS)
// JIHAD (webOS, cavekit-addons-extensions.md R7) — DELIVER INPUT IN THE PALM PROTOCOL.
//
// Everything above this point is a platform arm: XP_MACOSX, XP_WIN, MOZ_X11. A
// cairo-headless build matches none of them, so this function used to reduce to
// `return nsEventStatus_eIgnore;` and a windowless plugin received no mouse, key, focus or
// blur event in its whole lifetime. Measured on the TouchPad before this branch existed: a
// tap hit-tested the <EMBED>, the daemon logged `mouseSend <EMBED> ... : down`, and the
// plugin's NPP_HandleEvent was called exactly once ever — for the child's own gain-focus
// injection. Nothing upstream is at fault; the DOM listeners, the hit test, the windowless
// guards and the IPC hop are all live and correct (see the Dispatch*ToPlugin callers).
//
// The event shape is Palm's, because NPEvent on this platform IS Palm's union (npapi.h) and
// the device's Flash was compiled against that declaration.
{
NPEvent pluginEvent;
memset(&pluginEvent, 0, sizeof(pluginEvent));
if (anEvent.mClass == eMouseEventClass) {
const WidgetMouseEvent& mouseEvent = *anEvent.AsMouseEvent();
switch (anEvent.mMessage) {
case eMouseDown: pluginEvent.eventType = npPalmPenDownEvent; break;
case eMouseUp: pluginEvent.eventType = npPalmPenUpEvent; break;
case eMouseMove: pluginEvent.eventType = npPalmPenMoveEvent; break;
// The synthesized click/dblclick that follows a down/up pair. Palm's host sends these
// as their own event types rather than swallowing them (libWebKitLuna
// handleMouseEvent 0x4e946c-0x4e94d8 maps click -> 65536 and dblclick -> 32768, in
// ADDITION to the down/up pair, not instead of it), so this is what a plugin written
// for webOS was built to see. The X11 arm above drops them instead, which is right
// for X11 and wrong here.
//
// Flash itself ignores both: its NPP_HandleEvent jump table (libflashplayer.so
// 0x4514c-0x4522c) compares only against 1/2/4/8/16/64/128/256/512, and anything
// else falls through to "not handled" — confirmed on device, where a tap logs
// `palm event 0x10000 ... handled=0` between two handled pen events. Sending them
// anyway is deliberate: the contract is the host's, not this one plugin's, and a
// plugin that acts only on the click is otherwise silently unusable.
case eMouseClick: pluginEvent.eventType = npPalmPenClickEvent; break;
case eMouseDoubleClick: pluginEvent.eventType = npPalmPenDoubleClickEvent; break;
default: break; // over/out/wheel/aux: no Palm equivalent, drop
}
if (pluginEvent.eventType) {
// The plugin-local point, computed exactly as the MOZ_X11 arm above computes it. This
// is also what Palm's own host does — PluginView's webOS block uses the renderer's
// absoluteToLocal() on the same absolute location — so the two hosts agree on the
// space without either being ported to the other.
//
// Deliberately NO zoom or scroll arithmetic here. The card's fit-zoom is not an engine
// zoom (it is applied when the daemon scales the capture context), layout.css.
// devPixelsPerPx is pinned to 1.0, and the daemon has already mapped card px to CSS
// viewport px before dispatching. Adding a factor here is how plugin input drifts away
// from where page content thinks it was clicked; the criterion is that both go through
// the same transform, and they do precisely because this arm adds none.
const nsPresContext* presContext = mPluginFrame->PresContext();
nsPoint appPoint =
nsLayoutUtils::GetEventCoordinatesRelativeTo(&anEvent, mPluginFrame) -
mPluginFrame->GetContentRectRelativeToSelf().TopLeft();
pluginEvent.data.penEvent.xCoord =
presContext->AppUnitsToDevPixels(appPoint.x);
pluginEvent.data.penEvent.yCoord =
presContext->AppUnitsToDevPixels(appPoint.y);
pluginEvent.data.penEvent.modifiers =
(mouseEvent.IsControl() ? npPalmCtrlKeyModifier : 0) |
(mouseEvent.IsAlt() ? npPalmAltKeyModifier : 0) |
(mouseEvent.IsShift() ? npPalmShiftKeyModifier : 0) |
(mouseEvent.IsMeta() ? npPalmMetaKeyModifier : 0);
}
} else if (anEvent.mClass == eKeyboardEventClass) {
const WidgetKeyboardEvent& keyEvent = *anEvent.AsKeyboardEvent();
switch (anEvent.mMessage) {
case eKeyDown: pluginEvent.eventType = npPalmKeyDownEvent; break;
case eKeyUp: pluginEvent.eventType = npPalmKeyUpEvent; break;
case eKeyPress: pluginEvent.eventType = npPalmKeyPressEvent; break;
default: break;
}
if (pluginEvent.eventType) {
// There is no native key event to copy from on this platform (mPluginEvent is a
// Windows concept), so the Palm fields are synthesized from the widget event.
// rawkeyCode/rawModifier are Palm's "pass the unprocessed event through" pair, which
// for a synthesized event is the same key code and modifier set.
int32_t mods =
(keyEvent.IsControl() ? npPalmCtrlKeyModifier : 0) |
(keyEvent.IsAlt() ? npPalmAltKeyModifier : 0) |
(keyEvent.IsShift() ? npPalmShiftKeyModifier : 0) |
(keyEvent.IsMeta() ? npPalmMetaKeyModifier : 0);
pluginEvent.data.keyEvent.chr =
static_cast<int32_t>(keyEvent.PseudoCharCode());
pluginEvent.data.keyEvent.modifiers = mods;
pluginEvent.data.keyEvent.rawkeyCode = static_cast<int32_t>(keyEvent.mKeyCode);
pluginEvent.data.keyEvent.rawModifier = mods;
}
} else if (anEvent.mMessage == eFocus || anEvent.mMessage == eBlur) {
// Focus arrives as a bare WidgetGUIEvent with a null widget (see DispatchFocusToPlugin),
// so it must not touch the coordinate path above.
pluginEvent.eventType = npPalmSystemEvent;
pluginEvent.data.systemEvent.type =
(anEvent.mMessage == eFocus) ? npPalmGainFocusEvent : npPalmLoseFocusEvent;
}
if (pluginEvent.eventType) {
int16_t response = kNPEventNotHandled;
mInstance->HandleEvent(&pluginEvent, &response,
NS_PLUGIN_CALL_SAFE_TO_REENTER_GECKO);
if (response == kNPEventHandled) {
rv = nsEventStatus_eConsumeNoDefault;
}
}
}
#endif
return rv;
}
@@ -2977,23 +3162,40 @@ NS_IMETHODIMP nsPluginInstanceOwner::CreateWidget(void)
if (!mWidget) {
// native (single process)
mWidget = do_CreateInstance(kWidgetCID, &rv);
nsWidgetInitData initData;
initData.mWindowType = eWindowType_plugin;
initData.mUnicode = false;
initData.clipChildren = true;
initData.clipSiblings = true;
rv = mWidget->Create(parentWidget.get(), nullptr,
LayoutDeviceIntRect(0, 0, 0, 0), &initData);
if (NS_FAILED(rv)) {
mWidget->Destroy();
mWidget = nullptr;
return rv;
if (!mWidget) {
// JIHAD (2026-10-05): A BUILD WITH NO CHILD-WINDOW WIDGET HAS NO WINDOWED PLUGINS.
// The offscreen embedding registers no widget for kWidgetCID, so this came back
// null and the Create() call below dereferenced it: the daemon died with SIGSEGV on
// every page whose Flash movie was embedded the ordinary way. A plugin is windowed
// unless it asks otherwise, and Flash asks only for wmode=opaque/transparent — the
// default, wmode=window, is what most of the web uses (found on two "Flash test"
// sites; every test page of ours had wmode=opaque). webOS's own browser never had
// plugin windows either and handed every plugin a drawable; its Flash build draws
// through the Palm window structure in both modes. So do the same: with no window
// to give, the instance is windowless.
mInstance->SetWindowless(true);
windowless = true;
} else {
nsWidgetInitData initData;
initData.mWindowType = eWindowType_plugin;
initData.mUnicode = false;
initData.clipChildren = true;
initData.clipSiblings = true;
rv = mWidget->Create(parentWidget.get(), nullptr,
LayoutDeviceIntRect(0, 0, 0, 0), &initData);
if (NS_FAILED(rv)) {
mWidget->Destroy();
mWidget = nullptr;
return rv;
}
}
}
mWidget->EnableDragDrop(true);
mWidget->Show(false);
mWidget->Enable(false);
if (mWidget) {
mWidget->EnableDragDrop(true);
mWidget->Show(false);
mWidget->Enable(false);
}
}
if (mPluginFrame) {
+11 -1
View File
@@ -133,6 +133,12 @@ BrowserStreamChild::RecvNPP_DestroyStream(const NPReason& reason)
{
PLUGIN_LOG_DEBUG_METHOD;
// Jihad R7: end-of-stream is what tells a Flash player the movie is complete. A player that
// is never told stops on frame zero, which looks exactly like a player that cannot draw.
fprintf(stderr, "[jihad-npapi-child] RecvNPP_DestroyStream reason=%d state=%d\n",
(int)reason, (int)mState);
fflush(stderr);
if (ALIVE != mState)
NS_RUNTIMEABORT("Unexpected state: recevied NPP_DestroyStream twice?");
@@ -232,8 +238,12 @@ BrowserStreamChild::Deliver()
if (kStreamOpen == mStreamStatus)
mStreamStatus = NPRES_DONE;
(void) mInstance->mPluginIface
NPError dsrv = mInstance->mPluginIface
->destroystream(&mInstance->mData, &mStream, mStreamStatus);
// Jihad R7: the delivery site, as opposed to RecvNPP_DestroyStream which only queues it.
fprintf(stderr, "[jihad-npapi-child] NPP_DestroyStream status=%d rv=%d\n",
(int)mStreamStatus, (int)dsrv);
fflush(stderr);
}
if (DESTROYED == mDestroyPending && mNotifyPending) {
NS_ASSERTION(mStreamNotify, "mDestroyPending but no mStreamNotify?");
+74
View File
@@ -0,0 +1,74 @@
/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */
/* 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/. */
/*
* NPN entry points exported as GLOBAL SYMBOLS, for plugins that expect the old Netscape
* convention where the host browser exports them rather than only passing an NPNetscapeFuncs
* table to NP_Initialize.
*
* WHY THIS IS ITS OWN TRANSLATION UNIT. The obvious place for these is PluginModuleChild.cpp,
* and it does not work: npapi.h already declares NPN_InvalidateRect, and every header in this
* build is compiled under `-include config/gcc_hidden.h`, which pushes hidden visibility. GCC
* takes visibility from the FIRST declaration it sees, so a later definition carrying
* __attribute__((visibility("default"))) is silently ignored and the symbol lands in the
* object as GLOBAL HIDDEN — present to `nm`, absent from the dynamic table, and therefore
* impossible to interpose. Verified with readelf. This file deliberately includes no NPAPI
* headers at all and describes the functions with void* so there is no prior declaration to
* inherit hidden visibility from.
*
* WHY IT MATTERS. webOS's Flash imports NPN_CreateObject and NPN_InvalidateRect as undefined
* dynamic symbols. Left alone they bind into libWebKitLuna.so — another browser engine that
* is in this process because Flash (and our Piranha drawing path) pull it in. Two consequences,
* both measured:
* 1. Flash's repaint requests go to LunaSysMgr's WebKit and never reach our engine, so the
* plugin is only ever painted when something else happens to invalidate it. That alone
* makes an animating plugin look frozen.
* 2. It is a latent crash: UXP stores a PluginInstanceChild* in NPP::ndata, while
* libWebKitLuna's NPN_InvalidateRect casts that same field to a WebCore::PluginView* and
* makes a virtual call on it — a wild vtable dispatch on the first repaint.
*
* The forwarding pointers are installed by PluginModuleChild once the browser function table
* exists; until then these are safe no-ops.
*/
#include <stdio.h>
extern "C" {
void (*gJihadNPNInvalidateRect)(void* aNPP, void* aRect) = 0;
void* (*gJihadNPNCreateObject)(void* aNPP, void* aClass) = 0;
// Read by the host repaint clock in PluginInstanceChild.cpp: while this is zero the plugin has
// never asked to be repainted, so the host has to drive it.
unsigned gJihadNPNInvalidateRectCalls = 0;
__attribute__((visibility("default"))) void
NPN_InvalidateRect(void* aNPP, void* aRect)
{
// The first call is the signal that the plugin's frame clock is finally running; without
// it the plugin is only ever painted by an unrelated invalidation, which is
// indistinguishable on screen from a plugin that draws nothing. Log it once, and log the
// no-forwarder case too — arriving before PluginModuleChild installs the pointers is a
// dropped repaint, not a no-op.
gJihadNPNInvalidateRectCalls++;
static int sLogged = 0;
if (sLogged < 8) {
sLogged++;
fprintf(stderr, "[jihad-npapi-child] NPN_InvalidateRect npp=%p rect=%p fwd=%p\n",
aNPP, aRect, (void*)gJihadNPNInvalidateRect);
fflush(stderr);
}
if (gJihadNPNInvalidateRect) {
gJihadNPNInvalidateRect(aNPP, aRect);
}
}
__attribute__((visibility("default"))) void*
NPN_CreateObject(void* aNPP, void* aClass)
{
return gJihadNPNCreateObject ? gJihadNPNCreateObject(aNPP, aClass) : 0;
}
} // extern "C"
+23
View File
@@ -125,6 +125,29 @@ child:
// of its parent has changed
async ContentsScaleFactorChanged(double aContentsScaleFactor);
// Jihad (webOS, cavekit-addons-extensions.md R7): ask the plugin for ONE frame, now.
//
// Palm's host issued npPalmDrawEvent every time the PAGE painted, so Flash can sit on its own
// frame clock and expect to be asked. Patch 0026 got it drawing by giving the CHILD a timer,
// but that timer free-runs against the daemon's tick, so a frame lands at a drifting phase and
// waits up to a whole tick to be published. Measured on device with a 30 fps SWF: 1:1 delivery
// and a 31-34 ms average, but a gap spread of 9-62 ms, which is what the eye reads as stutter.
//
// CAUTION, measured 2026-08-10 and it contradicts what 0026 assumed: the plugin DOES invalidate
// itself, ~22 times a second, alongside our ~27. The counter 0026 reads to conclude otherwise
// (gJihadNPNInvalidateRectCalls) only sees calls that bind the NPN_InvalidateRect SYMBOL, and
// an ordinary NPAPI plugin goes through the NPNetscapeFuncs table instead. So this message is
// currently a SECOND producer rather than the only one, and whether it should exist at all for
// a self-invalidating plugin is open — see the R7 frame-rate criterion.
//
// This is the pull that removes the second clock. ASYNC on purpose: the stock host pulls the
// plugin synchronously inside its page paint (PluginViewPalm), but out-of-process that shape
// is intr NPP_HandleEvent, which would block the daemon's main thread — and therefore its
// tick and its YAP socket — for a full IPC round trip plus a Flash paint on every frame.
// Async keeps the phase lock and costs nothing: the child coalesces duplicate requests via
// mCurrentInvalidateTask, so an extra request while one is in flight is a no-op.
async JihadRequestFrame();
// ********************** Async plugins rendering
// see https://wiki.mozilla.org/Gecko:AsyncPluginPainting
// **********************
File diff suppressed because it is too large. Load diff
+23 -1
View File
@@ -36,7 +36,7 @@
#include <map>
#ifdef MOZ_WIDGET_GTK
#ifdef MOZ_ENABLE_NPAPI_GTK2
#include "gtk2xtbin.h"
#endif
@@ -141,6 +141,10 @@ protected:
virtual bool
RecvContentsScaleFactorChanged(const double& aContentsScaleFactor) override;
// Jihad: the host is driving the frame clock — draw one now. See PPluginInstance.ipdl.
virtual bool
RecvJihadRequestFrame() override;
virtual bool
AnswerNPP_Destroy(NPError* result) override;
@@ -462,6 +466,8 @@ private:
NPSetWindowCallbackStruct mWsInfo;
#ifdef MOZ_WIDGET_GTK
bool mXEmbed;
#endif
#ifdef MOZ_ENABLE_NPAPI_GTK2
XtClient mXtClient;
#endif
#elif defined(OS_WIN)
@@ -639,6 +645,22 @@ private:
// and p->getvalue return always false
bool mIsTransparent;
// Jihad R7 (webOS): npPalmGainFocusEvent is a once-per-instance startup signal, but the
// setwindow that carries it runs again on every resize and scroll.
bool mJihadSentPalmGainFocus;
// Jihad R7 (webOS): cap on the input-delivery breadcrumb in AnswerNPP_HandleEvent. A
// pen-move stream is unbounded; the daemon log is the only field-debug channel there is.
uint32_t mJihadInputEventsLogged;
public:
// Jihad R7 (webOS): repaint this instance from the host's own clock. See
// JihadGlibPumpTask::Run — a webOS plugin is not obliged to ask for repaints, because the
// host it was written for painted it as part of every page paint.
void JihadPalmRepaintTick();
private:
// Surface type optimized of parent process
gfxSurfaceType mSurfaceType;
+224 -7
View File
@@ -115,6 +115,16 @@ PluginInstanceParent::LookupPluginInstanceByID(uintptr_t aId)
}
#endif
// Jihad: every live instance, so a C entry point can reach one. jihad_plugin_frame_seq needs no
// instance because it only counts; a frame REQUEST has to be sent on an actual actor.
//
// A plain file-static array maintained in the ctor/dtor below, not a hashtable: this is at most a
// couple of entries on a single-card embedding, it is only ever touched on the main thread (the
// daemon calls the export from inside its own tick, on that same thread), and the alternative —
// walking nsPluginHost's nsNPAPIPluginInstance list the way jihad_plugin_palm_spotlight does —
// would still have to get from there to the PluginInstanceParent actor to call Send on it.
static nsTArray<PluginInstanceParent*>* gJihadLiveInstances = nullptr;
PluginInstanceParent::PluginInstanceParent(PluginModuleParent* parent,
NPP npp,
const nsCString& aMimeType,
@@ -145,10 +155,24 @@ PluginInstanceParent::PluginInstanceParent(PluginModuleParent* parent,
sPluginInstanceList = new nsClassHashtable<nsVoidPtrHashKey, PluginInstanceParent>();
}
#endif
// Jihad: join the live list that jihad_plugin_request_frame walks. Leaked on purpose — it
// outlives every instance and there is no shutdown hook here that is guaranteed to run
// after the last dtor.
if (!gJihadLiveInstances) {
gJihadLiveInstances = new nsTArray<PluginInstanceParent*>();
}
gJihadLiveInstances->AppendElement(this);
}
PluginInstanceParent::~PluginInstanceParent()
{
// Jihad: leave the live list FIRST. Anything below can run script or spin the loop, and a
// frame request delivered to a half-destroyed actor is a use-after-free rather than a
// dropped frame.
if (gJihadLiveInstances) {
gJihadLiveInstances->RemoveElement(this);
}
if (mNPP)
mNPP->pdata = nullptr;
@@ -269,6 +293,8 @@ PluginInstanceParent::AnswerNPN_GetValue_NPNVnetscapeWindow(NativeWindowHandle*
XID id;
#elif defined(XP_DARWIN)
intptr_t id;
#elif defined(MOZ_WIDGET_HEADLESS)
intptr_t id; // Jihad headless: no native window id
#else
#warning Implement me
#endif
@@ -769,6 +795,51 @@ PluginInstanceParent::RecvShowDirectBitmap(Shmem&& buffer,
return false;
}
// NO COMPOSITOR PROCESS: composite the plugin's bitmap directly.
//
// Everything below this point routes the plugin's pixels through a TextureClient to a
// compositor, and that requires ImageBridgeChild — which only exists with off-main-thread
// composition. This embedder deliberately runs OMTC off (the offscreen renderer uses the
// in-process BasicLayerManager and hands finished frames to the browser adapter itself),
// so ImageBridgeChild::GetSingleton() is null and TextureClientRecycleAllocator is built
// around a null KnowsCompositor. The first plugin paint then dereferenced it and took the
// whole daemon down with it — upstream never sees this because the DXGI path a few
// hundred lines up checks the forwarder and the direct-bitmap path does not.
//
// A BasicLayerManager does not need a TextureClient at all: it draws an ImageLayer from
// the image's own SourceSurface. So wrap the pixels in a plain SourceSurfaceImage and
// hand that to the container.
//
// The pixels are COPIED, not wrapped. `source` above wraps the shmem the plugin drew
// into, and the plugin owns that buffer and may draw the next frame into it as soon as
// this call returns; the texture path's UpdateFromSurface copies for the same reason.
if (!ImageBridgeChild::GetSingleton()) {
RefPtr<gfx::DataSourceSurface> copy =
gfx::Factory::CreateDataSourceSurface(size, format);
if (!copy) {
NS_WARNING("Could not allocate a surface for plugin bitmap!");
return false;
}
{
gfx::DataSourceSurface::ScopedMap srcMap(source, gfx::DataSourceSurface::READ);
gfx::DataSourceSurface::ScopedMap dstMap(copy, gfx::DataSourceSurface::WRITE);
if (!srcMap.IsMapped() || !dstMap.IsMapped()) {
return false;
}
const int32_t bpp = BytesPerPixel(format);
const int32_t rowBytes = size.width * bpp;
for (int32_t y = 0; y < size.height; y++) {
memcpy(dstMap.GetData() + (ptrdiff_t)y * dstMap.GetStride(),
srcMap.GetData() + (ptrdiff_t)y * srcMap.GetStride(),
rowBytes);
}
}
RefPtr<SourceSurfaceImage> image = new SourceSurfaceImage(size, copy);
SetCurrentImage(image);
return true;
}
// Allocate a texture for the compositor.
RefPtr<TextureClientRecycleAllocator> allocator = mParent->EnsureTextureAllocatorForDirectBitmap();
RefPtr<TextureClient> texture = allocator->CreateOrRecycle(
@@ -864,6 +935,55 @@ PluginInstanceParent::RecvShowDirectDXGISurface(const WindowsHandle& handle,
}
#endif // MOZ_ENABLE_NPAPI
// Jihad: monotonic count of plugin frames that have reached the parent. Process-wide rather
// than per-instance, matching how the daemon consumes it — the embedding is single-page and the
// question it answers is "has ANY plugin produced a new frame since I last published". A plain
// uint32_t incremented and read on the same (main) thread; the daemon reads it from the same
// thread inside its tick, so no atomics are needed and none are used elsewhere in this bridge.
static uint32_t gJihadPluginFrameSeq = 0;
extern "C" {
// Weak-linked from the daemon (render/goanna/GoannaRenderPage.cpp), the same way
// jihad_offscreen_take_dirty is: an older libxul simply does not export it, and the daemon then
// falls back to its dirty-flag behaviour instead of failing to load.
MOZ_EXPORT uint32_t jihad_plugin_frame_seq(void)
{
return gJihadPluginFrameSeq;
}
}
extern "C" {
// Ask every live plugin for one frame. Returns how many were asked, so the daemon can tell "no
// plugin" from "delivered" — the same contract as jihad_plugin_palm_spotlight.
//
// WEAK on the daemon side: a libxul without this symbol keeps the child's own timer as the frame
// clock (patch 0026 behaviour), which is slower to settle but never wrong.
MOZ_EXPORT int jihad_plugin_request_frame(void)
{
#ifdef MOZ_ENABLE_NPAPI
if (!gJihadLiveInstances) {
return 0;
}
// Copy first: SendJihadRequestFrame can fail and tear the actor down, which would mutate the
// array underneath the loop. Same reasoning as the instance-list copy in
// jihad_plugin_palm_spotlight (nsPluginHost.cpp).
nsTArray<PluginInstanceParent*> snap(*gJihadLiveInstances);
int asked = 0;
for (uint32_t i = 0; i < snap.Length(); ++i) {
if (!gJihadLiveInstances->Contains(snap[i])) {
continue; // torn down by an earlier Send in this same loop
}
if (snap[i]->SendJihadRequestFrame()) {
asked++;
}
}
return asked;
#else
return 0;
#endif
}
}
bool
PluginInstanceParent::RecvShow(const NPRect& updatedRect,
const SurfaceDescriptor& newSurface,
@@ -877,6 +997,27 @@ PluginInstanceParent::RecvShow(const NPRect& updatedRect,
MOZ_ASSERT(!IsUsingDirectDrawing());
// FRAME-RATE READOUT, parent side. Paired with the child's `palm draw:` line: the child
// reports how often the plugin was ASKED to draw, this reports how many of those frames
// actually crossed the process boundary. A gap between the two numbers is the difference
// between "the plugin is slow" and "we are dropping its frames", which are opposite bugs.
{
static uint32_t sShows = 0;
static uint32_t sLastReport = 0;
sShows++;
uint32_t nowMs = uint32_t(PR_IntervalToMilliseconds(PR_IntervalNow()));
if (!sLastReport) {
sLastReport = nowMs;
} else if (nowMs - sLastReport >= 2000) {
fprintf(stderr, "[jihad-npapi] RecvShow: %u in %ums (%.1f fps)\n",
sShows, nowMs - sLastReport,
sShows * 1000.0 / double(nowMs - sLastReport));
fflush(stderr);
sShows = 0;
sLastReport = nowMs;
}
}
// XXXjwatt rewrite to use Moz2D
RefPtr<gfxASurface> surface;
if (newSurface.type() == SurfaceDescriptor::TShmem) {
@@ -959,14 +1100,75 @@ PluginInstanceParent::RecvShow(const NPRect& updatedRect,
updatedRect.bottom - updatedRect.top);
surface->MarkDirty(ur);
bool isPlugin = true;
RefPtr<gfx::SourceSurface> sourceSurface =
gfxPlatform::GetPlatform()->GetSourceSurfaceForSurface(nullptr, surface, isPlugin);
RefPtr<SourceSurfaceImage> image = new SourceSurfaceImage(surface->GetSize(), sourceSurface);
// JIHAD (2026-08-09): route the legacy windowless show through the SAME
// representation the direct-bitmap path uses, for the SAME reasons.
//
// Background: the webOS npPalmDrawEvent path (PaintRectToPlatformSurface) makes Flash
// blit its stage into mCurrentSurface — measured full-magenta in the child — and ships
// it here via SendShow. The control plugin's pixels reach the screen through
// RecvShowDirectBitmap; Flash's did not, although both feed the one mImageContainer the
// BasicLayerManager reads. Two concrete differences made the legacy path invisible:
// 1. It WRAPPED the shmem (GetSourceSurfaceForSurface(nullptr,…)) instead of copying.
// The child double-buffers and SwapSurfaces() immediately after SendShow returns,
// so a wrapped front buffer is the very buffer the child repaints next — the image
// the layer later samples is racing the child. RecvShowDirectBitmap copies for
// exactly this reason ("the plugin owns that buffer and may draw the next frame
// into it as soon as this call returns").
// 2. It stamped no frameID. SetCurrentImage() on the working path assigns a monotonic
// ++mFrameID; the ImageContainer/ImageLayer treats a repeated frameID as "no new
// frame". A fixed 0 can be coalesced away.
// Copying into a fresh DataSourceSurface and stamping a monotonic frameID makes this
// path identical in substance to the one already proven to composite here.
IntSize ssize = surface->GetSize();
RefPtr<Image> image;
RefPtr<gfxImageSurface> imgSurf = surface->GetAsImageSurface();
if (imgSurf) {
// Always present the copy as B8G8R8A8, and force the alpha channel opaque when the
// source carries no alpha (RGB24 / B8G8R8X8). This is Bug 1196927 in this file's own
// words (see PluginInstanceChild::PaintRectToSurface): Cairo/Skia leave the X byte of
// a BGRX surface zeroed, and a BasicImageLayer then composites those pixels multiplied
// by alpha 0 — i.e. fully transparent. That is exactly why Flash's magenta stage
// arrived here (RecvShow logged px0=0xffff00ff) yet BasicImageLayer::Paint drew
// nothing visible, while the control plugin — which ships B8G8R8A8 — composited fine
// through the identical layer path. webOS windowless plugins are opaque
// (wmode=opaque forces the windowless instance in the first place), so forcing alpha
// opaque is correct here.
const SurfaceFormat srcFmt = surface->GetSurfaceFormat();
const bool srcHasAlpha =
srcFmt != SurfaceFormat::B8G8R8X8 &&
srcFmt != SurfaceFormat::R8G8B8X8;
RefPtr<gfx::DataSourceSurface> copy =
gfx::Factory::CreateDataSourceSurface(ssize, SurfaceFormat::B8G8R8A8);
if (copy) {
gfx::DataSourceSurface::ScopedMap dstMap(copy, gfx::DataSourceSurface::WRITE);
if (dstMap.IsMapped()) {
const int32_t rowBytes = ssize.width * 4;
for (int32_t y = 0; y < ssize.height; y++) {
uint8_t* dstRow = dstMap.GetData() + (ptrdiff_t)y * dstMap.GetStride();
memcpy(dstRow, imgSurf->Data() + (ptrdiff_t)y * imgSurf->Stride(),
rowBytes);
if (!srcHasAlpha) {
for (int32_t x = 0; x < ssize.width; x++) {
dstRow[x * 4 + 3] = 0xff; // BGRA: byte 3 is alpha
}
}
}
image = new SourceSurfaceImage(ssize, copy);
}
}
}
if (!image) {
// Fallback to the original wrap if the copy path could not be taken.
bool isPlugin = true;
RefPtr<gfx::SourceSurface> sourceSurface =
gfxPlatform::GetPlatform()->GetSourceSurfaceForSurface(nullptr, surface, isPlugin);
image = new SourceSurfaceImage(ssize, sourceSurface);
}
ImageContainer::NonOwningImage holder(image);
holder.mFrameID = ++mFrameID;
AutoTArray<ImageContainer::NonOwningImage,1> imageList;
imageList.AppendElement(
ImageContainer::NonOwningImage(image));
imageList.AppendElement(holder);
ImageContainer *container = GetImageContainer();
container->SetCurrentImages(imageList);
@@ -976,6 +1178,17 @@ PluginInstanceParent::RecvShow(const NPRect& updatedRect,
}
mFrontSurface = surface;
// Jihad: count DELIVERED plugin frames so the embedder can tell a new one from a repeat.
// The daemon's only frame signal is PuppetWidget's sticky dirty boolean, which carries no
// identity: the refresh driver and ordinary page damage set the same flag, so "there is
// damage" cannot answer "did the plugin draw again". Measured 2026-08-10 on a 30 fps SWF,
// that ambiguity is what made both available policies wrong — publishing on every dirty
// tick sent ~78 card frames for ~60 plugin frames (duplicates, and 22-44 deferred because
// the adapter could not drain them), while pacing on a fixed 33 ms grid sent ~57 for ~68
// (dropped frames, so the animation stepped two positions at once). The stock webOS host
// never had to choose: it pulls the plugin synchronously inside the page paint, so it is
// 1:1 by construction. This counter is what lets the daemon be 1:1 too.
++gJihadPluginFrameSeq;
RecvNPN_InvalidateRect(updatedRect);
PLUGIN_LOG_DEBUG((" (RecvShow invalidated for surface %p)",
@@ -1301,7 +1514,11 @@ PluginInstanceParent::GetImageContainer()
return mImageContainer;
}
if (IsUsingDirectDrawing()) {
if (IsUsingDirectDrawing() && ImageBridgeChild::GetSingleton()) {
// ASYNCHRONOUS means "shared with the compositor through ImageBridge". Without an
// ImageBridge the async container silently gets no image client and never publishes
// anything, so with OMTC off this has to be an ordinary local container — which is
// what the BasicLayerManager reads from. See RecvShowDirectBitmap.
mImageContainer = LayerManager::CreateImageContainer(ImageContainer::ASYNCHRONOUS);
} else {
mImageContainer = LayerManager::CreateImageContainer();
+2
View File
@@ -117,6 +117,8 @@ typedef HWND NativeWindowHandle;
typedef XID NativeWindowHandle;
#elif defined(XP_DARWIN)
typedef intptr_t NativeWindowHandle; // never actually used, will always be 0
#elif defined(MOZ_WIDGET_HEADLESS)
typedef intptr_t NativeWindowHandle; // Jihad headless: no native window handle
#else
#error Need NativeWindowHandle for this platform
#endif
+51 -1
View File
@@ -12,6 +12,9 @@
#ifdef MOZ_WIDGET_GTK
#include <gtk/gtk.h>
#if (GTK_MAJOR_VERSION == 3)
#include <gtk/gtkx.h>
#endif
#endif
#include "nsIFile.h"
@@ -57,6 +60,9 @@ using namespace mozilla::ipc;
using namespace mozilla::plugins;
using namespace mozilla::widget;
// Defined at the bottom of this file; see the NPN SYMBOL INTERPOSITION block there.
namespace mozilla { namespace plugins { void JihadInstallNPNInterposition(); } }
#if defined(XP_WIN)
const wchar_t * kFlashFullscreenClass = L"ShockwaveFlashFullScreen";
const wchar_t * kMozillaWindowClass = L"MozillaWindowClass";
@@ -229,6 +235,10 @@ PluginModuleChild::InitForChrome(const std::string& aPluginFilename,
{
NS_ASSERTION(aChannel, "need a channel");
// Point the exported NPN_* globals at the real implementations before any plugin can be
// loaded, so a plugin that resolves them as symbols reaches us rather than libWebKitLuna.
JihadInstallNPNInterposition();
if (!InitGraphics())
return false;
@@ -248,7 +258,11 @@ PluginModuleChild::InitForChrome(const std::string& aPluginFilename,
nsPluginFile pluginFile(localFile);
nsPluginInfo info = nsPluginInfo();
if (NS_FAILED(pluginFile.GetPluginInfo(info, &mLibrary))) {
nsresult jihadRv = pluginFile.GetPluginInfo(info, &mLibrary);
fprintf(stderr, "[jihad-npapi-child] GetPluginInfo rv=0x%x lib=%d\n",
(unsigned)jihadRv, (int)!!mLibrary);
fflush(stderr);
if (NS_FAILED(jihadRv)) {
return false;
}
@@ -1101,7 +1115,11 @@ _getvalue(NPP aNPP,
return InstCast(aNPP)->NPN_GetValue(aVariable, aValue);
}
else {
#ifdef MOZ_ENABLE_NPAPI_GTK2
*(void **)aValue = xt_client_get_display();
#else
*(void **)aValue = DefaultXDisplay();
#endif
}
return NPERR_NO_ERROR;
}
@@ -1836,6 +1854,8 @@ bool
PluginModuleChild::AnswerNP_Initialize(const PluginSettings& aSettings, NPError* rv)
{
*rv = DoNP_Initialize(aSettings);
fprintf(stderr, "[jihad-npapi-child] AnswerNP_Initialize rv=%d\n", (int)*rv);
fflush(stderr);
return true;
}
@@ -1875,6 +1895,8 @@ PluginModuleChild::DoNP_Initialize(const PluginSettings& aSettings)
# error Please implement me for your platform
#endif
fprintf(stderr, "[jihad-npapi-child] DoNP_Initialize plugin returned %d\n", (int)result);
fflush(stderr);
return result;
}
@@ -2610,3 +2632,31 @@ PluginModuleChild::RecvNPP_SetValue_NPNVaudioDeviceChangeDetails(
#endif
}
#endif // MOZ_ENABLE_NPAPI
// ── NPN SYMBOL INTERPOSITION (Jihad, addons R7) ──────────────────────────────────────────
//
// The exported NPN_* globals themselves live in JihadNPNInterpose.cpp (they must, to escape
// hidden visibility — see the long comment there). All that is needed here is to point them
// at the same implementations the browser function table uses, which is done once the table
// exists. Installed from a static initializer-free helper called by the child at startup.
extern "C" {
extern void (*gJihadNPNInvalidateRect)(void*, void*);
extern void* (*gJihadNPNCreateObject)(void*, void*);
}
namespace mozilla {
namespace plugins {
void JihadInstallNPNInterposition()
{
const NPNetscapeFuncs& f = PluginModuleChild::sBrowserFuncs;
gJihadNPNInvalidateRect = (void(*)(void*, void*))f.invalidaterect;
gJihadNPNCreateObject = (void*(*)(void*, void*))f.createobject;
fprintf(stderr, "[jihad-npapi-child] NPN interposition installed invalidaterect=%p "
"createobject=%p\n",
(void*)f.invalidaterect, (void*)f.createobject);
fflush(stderr);
}
} // namespace plugins
} // namespace mozilla
+190 -1
View File
@@ -444,6 +444,8 @@ PluginModuleChromeParent::LoadModule(const char* aFilePath, uint32_t aPluginId,
parent->mSubprocess->SetCallRunnableImmediately(!parent->mIsStartingAsync);
bool launched = parent->mSubprocess->Launch(Move(onLaunchedRunnable),
aPluginTag->mSandboxLevel);
fprintf(stderr, "[jihad-npapi] LoadModule path=%s async=%d launched=%d\n",
aFilePath, (int)parent->mIsStartingAsync, (int)launched);
if (!launched) {
// We never reached open
parent->mShutdown = true;
@@ -455,7 +457,10 @@ PluginModuleChromeParent::LoadModule(const char* aFilePath, uint32_t aPluginId,
parent->mIsBlocklisted = NS_FAILED(rv) || blocklistState != 0;
if (!parent->mIsStartingAsync) {
int32_t launchTimeoutSecs = Preferences::GetInt(kLaunchTimeoutPref, 0);
if (!parent->mSubprocess->WaitUntilConnected(launchTimeoutSecs * 1000)) {
bool connected = parent->mSubprocess->WaitUntilConnected(launchTimeoutSecs * 1000);
fprintf(stderr, "[jihad-npapi] LoadModule WaitUntilConnected(%ds) = %d\n",
(int)launchTimeoutSecs, (int)connected);
if (!connected) {
parent->mShutdown = true;
return nullptr;
}
@@ -1134,9 +1139,20 @@ PluginModuleChromeParent::OnHangUIContinue()
}
#endif // XP_WIN
// Defined with the rest of the Jihad CPU-boost helpers further down this file.
namespace { void JihadCpuBoostReleaseAll(); }
void
PluginModuleParent::ActorDestroy(ActorDestroyReason why)
{
// Release the CPU boost here as well as in NPP_Destroy, because on this port NPP_Destroy is
// NOT the path a plugin usually leaves by: navigating away from a Flash page ends in the
// child's own mozalloc_abort, so the instance never gets an orderly destroy and the boost
// would leak — device-observed 2026-08-10, tunables still at 40/50000 after navigation with
// no release logged. This runs for every ActorDestroyReason, normal and abnormal, which also
// covers a plugin crash.
JihadCpuBoostReleaseAll();
switch (why) {
case AbnormalShutdown: {
mShutdown = true;
@@ -1271,6 +1287,172 @@ NP_BEGIN_MACRO \
return i->func; \
NP_END_MACRO
// ── Jihad: hold a CPU-frequency boost while a plugin instance is alive ──────────────────────
//
// WHY. The TouchPad's governor is Palm's `ondemandtcl`, whose stock `up_threshold` is 95 — it
// only raises the clock when a single core exceeds 95% load. Flash never gets there: measured on
// device, the daemon runs ~26% and plugin-container ~11% of two cores, so a passively-watched
// animation sits at the FLOOR. Sampling `scaling_cur_freq` once a second during playback gave
// 192000-384000 out of a 1188000 maximum, snapping back to 1188000 the moment the page was
// navigated away. The whole system was only 69% busy, so this was never a capacity wall.
//
// Dropping `up_threshold` to 40 and `sampling_rate` to 50000 pins 1188000 and lifts the card
// composite from 20.4-27.5 fps to 30.7-35.2, daemon paints 24.0-28.7 to 31.4-33.0, and the frame
// gap MAXIMUM — the number that reads as stutter — from 63-81 ms to 49-56. It also clears the
// audio xruns, because MP3 decode at 192 MHz underruns.
//
// NEVER WRITE `scaling_governor`. Switching away from `ondemandtcl` DEADLOCKS cpufreq in the
// kernel: every later reader of any cpufreq node blocks in unkillable D state — the daemon,
// `powerlog`, and the restore path itself — so the boost is never released and only a reboot
// clears it (and a clean `reboot` will not run, because init waits on those D-state tasks).
// Governor TUNABLE writes, which is all this does, are safe and return instantly.
//
// These are SYSTEM-WIDE tunables, so they are held only while a plugin instance exists and the
// ORIGINAL values are read back and restored on teardown. The upstart job restores them again
// from `post-stop`, because a daemon that dies holding the boost would otherwise leave the whole
// device tuned for battery burn. Both hooks run on the main thread, so the counter needs no lock.
namespace {
const char kJihadOndemandDir[] = "/sys/devices/system/cpu/cpufreq/ondemandtcl";
// The stock values, used ONLY as the restore fallback when the current value already equals what
// we are about to write — which happens when another variant's daemon is holding the boost.
// Saving the boosted value as "original" there would make the last release pin the device.
const char kJihadStockUpThreshold[] = "95";
const char kJihadStockSamplingRate[] = "200000";
// The instances that actually TOOK a boost, not a bare count. NPP_Destroy also runs for instances
// that failed before the boost was taken, and decrementing a count there would release the boost
// out from under a plugin that is still playing.
nsTArray<NPP> gJihadBoostedInstances;
bool gJihadBoostHeld = false;
char gJihadSavedUpThreshold[32];
char gJihadSavedSamplingRate[32];
bool
JihadReadTunable(const char* aName, char* aOut, size_t aOutLen)
{
char path[256];
snprintf(path, sizeof(path), "%s/%s", kJihadOndemandDir, aName);
FILE* f = fopen(path, "r");
if (!f) return false;
char* got = fgets(aOut, aOutLen, f);
fclose(f);
if (!got) return false;
// Trim the trailing newline so a value read back can be compared and rewritten verbatim.
size_t n = strlen(aOut);
while (n > 0 && (aOut[n - 1] == '\n' || aOut[n - 1] == '\r')) aOut[--n] = '\0';
return n > 0;
}
bool
JihadWriteTunable(const char* aName, const char* aValue)
{
char path[256];
snprintf(path, sizeof(path), "%s/%s", kJihadOndemandDir, aName);
FILE* f = fopen(path, "w");
if (!f) return false;
bool ok = fputs(aValue, f) >= 0;
ok = (fclose(f) == 0) && ok;
return ok;
}
// Env-tunable for the same reason the repaint divisor is: the right value is a property of the
// device under load, not of the source, and a rebuild per candidate costs a 44 MB push.
void
JihadCpuBoostTargets(const char** aUpThreshold, const char** aSamplingRate)
{
static char sUp[32] = "40";
static char sRate[32] = "50000";
static bool sInit = false;
if (!sInit) {
sInit = true;
const char* e = getenv("JIHAD_PLUGIN_CPU_UP_THRESHOLD");
if (e && *e) {
int v = atoi(e);
if (v >= 5 && v <= 95) snprintf(sUp, sizeof(sUp), "%d", v);
}
e = getenv("JIHAD_PLUGIN_CPU_SAMPLING_RATE");
if (e && *e) {
int v = atoi(e);
if (v >= 10000 && v <= 1000000) snprintf(sRate, sizeof(sRate), "%d", v);
}
}
*aUpThreshold = sUp;
*aSamplingRate = sRate;
}
bool
JihadCpuBoostEnabled()
{
const char* e = getenv("JIHAD_PLUGIN_CPU_BOOST");
return !(e && e[0] == '0' && e[1] == '\0');
}
void
JihadCpuBoostAcquire(NPP aInstance)
{
if (!JihadCpuBoostEnabled() || gJihadBoostedInstances.Contains(aInstance)) return;
gJihadBoostedInstances.AppendElement(aInstance);
if (gJihadBoostedInstances.Length() != 1 || gJihadBoostHeld) return;
const char* wantUp;
const char* wantRate;
JihadCpuBoostTargets(&wantUp, &wantRate);
if (!JihadReadTunable("up_threshold", gJihadSavedUpThreshold,
sizeof(gJihadSavedUpThreshold)) ||
!strcmp(gJihadSavedUpThreshold, wantUp)) {
strcpy(gJihadSavedUpThreshold, kJihadStockUpThreshold);
}
if (!JihadReadTunable("sampling_rate", gJihadSavedSamplingRate,
sizeof(gJihadSavedSamplingRate)) ||
!strcmp(gJihadSavedSamplingRate, wantRate)) {
strcpy(gJihadSavedSamplingRate, kJihadStockSamplingRate);
}
bool ok = JihadWriteTunable("up_threshold", wantUp);
ok = JihadWriteTunable("sampling_rate", wantRate) && ok;
gJihadBoostHeld = ok;
fprintf(stderr, "[jihad-npapi] cpu boost acquire: up_threshold %s->%s sampling_rate %s->%s%s\n",
gJihadSavedUpThreshold, wantUp, gJihadSavedSamplingRate, wantRate,
ok ? "" : " FAILED (not root, or no ondemandtcl)");
fflush(stderr);
}
void
JihadCpuBoostRelease(NPP aInstance)
{
if (!gJihadBoostedInstances.RemoveElement(aInstance)) return;
if (!gJihadBoostedInstances.IsEmpty() || !gJihadBoostHeld) return;
JihadWriteTunable("up_threshold", gJihadSavedUpThreshold);
JihadWriteTunable("sampling_rate", gJihadSavedSamplingRate);
gJihadBoostHeld = false;
fprintf(stderr, "[jihad-npapi] cpu boost release: up_threshold=%s sampling_rate=%s\n",
gJihadSavedUpThreshold, gJihadSavedSamplingRate);
fflush(stderr);
}
// Drop every outstanding boost at once. Used when the plugin module itself goes away, which on
// this port is the NORMAL exit path (see ActorDestroy), not just the crash path.
void
JihadCpuBoostReleaseAll()
{
if (gJihadBoostedInstances.IsEmpty()) return;
gJihadBoostedInstances.Clear();
if (!gJihadBoostHeld) return;
JihadWriteTunable("up_threshold", gJihadSavedUpThreshold);
JihadWriteTunable("sampling_rate", gJihadSavedSamplingRate);
gJihadBoostHeld = false;
fprintf(stderr, "[jihad-npapi] cpu boost release (module gone): up_threshold=%s sampling_rate=%s\n",
gJihadSavedUpThreshold, gJihadSavedSamplingRate);
fflush(stderr);
}
} // anonymous namespace
NPError
PluginModuleParent::NPP_Destroy(NPP instance,
NPSavedData** saved)
@@ -1296,6 +1478,7 @@ PluginModuleParent::NPP_Destroy(NPP instance,
instance->pdata = nullptr;
Unused << PluginInstanceParent::Call__delete__(parentInstance);
JihadCpuBoostRelease(instance);
return retval;
}
@@ -2292,6 +2475,12 @@ PluginModuleParent::NPP_NewInternal(NPMIMEType pluginType, NPP instance,
UpdatePluginTimeout();
// Taken ONLY here, on the single success path, and released in NPP_Destroy. Every failure
// return above this point either deletes |parentInstance| itself or hands it to NPP_Destroy,
// and a boost acquired on one of those paths would never be released — which on this device
// means leaving the WHOLE system tuned for battery burn with nothing owning it.
JihadCpuBoostAcquire(instance);
return NS_OK;
}
+5
View File
@@ -111,9 +111,14 @@ PluginProcessChild::Init()
return false;
}
fprintf(stderr, "[jihad-npapi-child] InitForChrome plugin=%s parentPid=%d\n",
pluginFilename.c_str(), (int)ParentPid());
fflush(stderr);
bool retval = mPlugin.InitForChrome(pluginFilename, ParentPid(),
IOThreadChild::message_loop(),
IOThreadChild::channel());
fprintf(stderr, "[jihad-npapi-child] InitForChrome = %d\n", (int)retval);
fflush(stderr);
#if defined(XP_MACOSX)
if (nsCocoaFeatures::OnYosemiteOrLater()) {
// Explicitly turn off CGEvent logging. This works around bug 1092855.
+9 -3
View File
@@ -88,6 +88,10 @@ UNIFIED_SOURCES += [
]
SOURCES += [
# Jihad: must NOT be unified — it deliberately includes no NPAPI headers so its exported
# NPN_* symbols escape the hidden-visibility pragma. Merging it with a TU that includes
# npapi.h would reintroduce the prior hidden declaration and silently un-export them.
'JihadNPNInterpose.cpp',
'PluginInstanceChild.cpp', # 'PluginThreadCallback' : ambiguous symbol
'PluginModuleChild.cpp', # Redefinition of mozilla::WindowsDllInterceptor sUser32Intercept
]
@@ -143,11 +147,13 @@ LOCAL_INCLUDES += [
DEFINES['FORCE_PR_LOG'] = True
if CONFIG['MOZ_WIDGET_TOOLKIT'] != 'gtk3':
CXXFLAGS += CONFIG['TK_CFLAGS']
else:
if (CONFIG['MOZ_WIDGET_TOOLKIT'] == 'gtk3' and
CONFIG['MOZ_ENABLE_NPAPI'] and
CONFIG['MOZ_ENABLE_NPAPI_GTK2']):
# Force build against gtk+2 for struct offsets and such.
CXXFLAGS += CONFIG['MOZ_GTK2_CFLAGS']
else:
CXXFLAGS += CONFIG['TK_CFLAGS']
CXXFLAGS += CONFIG['MOZ_CAIRO_CFLAGS']
+10
View File
@@ -5,6 +5,7 @@
// Local Includes
#include "nsDocShellTreeOwner.h"
#include "nsIBadCertListener2.h" // JIHAD: route cert errors to the embedder chrome
#include "nsWebBrowser.h"
// Helper Classes
@@ -143,6 +144,15 @@ nsDocShellTreeOwner::GetInterface(const nsIID& aIID, void** aSink)
return NS_OK;
}
if (aIID.Equals(NS_GET_IID(nsIBadCertListener2))) {
// JIHAD: the embedder's chrome is what implements this — see the matching comment in
// nsDocShell::GetInterface. Same forwarding shape as nsIWebBrowserChromeFocus below.
if (mWebBrowserChromeWeak != nullptr) {
return mWebBrowserChromeWeak->QueryReferent(aIID, aSink);
}
return mOwnerWin ? mOwnerWin->QueryInterface(aIID, aSink) : NS_NOINTERFACE;
}
if (aIID.Equals(NS_GET_IID(nsIWebBrowserChromeFocus))) {
if (mWebBrowserChromeWeak != nullptr) {
return mWebBrowserChromeWeak->QueryReferent(aIID, aSink);
+36 -3
View File
@@ -390,6 +390,14 @@ nsWebBrowserFind::SetSelectionAndScroll(nsPIDOMWindowOuter* aWindow,
nsCOMPtr<nsISelection> selection;
// JIHAD: frame->GetSelectionController can leave selCon NULL, and this dereferenced it
// unconditionally — a null deref that took the whole process down. It is reachable in an
// offscreen embedding with no chrome: this is the ONLY thing that made find-in-page
// unusable there (faultaddr=0x0, measured repeatedly). Guarding costs nothing and the
// rest of the function already tolerates a null `selection`.
if (!selCon) {
return;
}
selCon->SetDisplaySelection(nsISelectionController::SELECTION_ON);
selCon->GetSelection(nsISelectionController::SELECTION_NORMAL,
getter_AddRefs(selection));
@@ -707,7 +715,24 @@ nsWebBrowserFind::SearchInFrame(nsPIDOMWindowOuter* aWindow, bool aWrapping,
return NS_ERROR_FAILURE;
}
if (!nsContentUtils::SubjectPrincipal()->Subsumes(theDoc->NodePrincipal())) {
// JIHAD: this same-origin check is why find-in-page took the whole process down in this
// embedding, and the engine says why itself. nsContentUtils::SubjectPrincipal() begins:
// JSContext* cx = GetCurrentJSContext();
// if (!cx) {
// MOZ_CRASH("Accessing the Subject Principal without an AutoJSAPI on the stack is forbidden");
// }
// An EMBEDDER calls nsIWebBrowserFind::FindNext from C++ with no script on the stack, so
// there is no JSContext and MOZ_CRASH fires — a deliberate abort that presents as SIGSEGV at
// address 0, which is precisely the fault this port recorded for two weeks and attributed to
// "a frame-selection controller the offscreen browser doesn't set up". That was wrong: the
// selection machinery is fine and the crash happens BEFORE any of it, on the security check.
//
// The check is meaningful only when there IS a caller to check. With no JS on the stack the
// caller is native embedding code, which is already fully trusted — it is inside libxul's own
// process, holding the nsIWebBrowserFind for that very docShell. So: consult the subject
// principal only when a JSContext exists, and otherwise proceed.
if (nsContentUtils::GetCurrentJSContext() &&
!nsContentUtils::SubjectPrincipal()->Subsumes(theDoc->NodePrincipal())) {
return NS_ERROR_DOM_PROP_ACCESS_DENIED;
}
@@ -809,8 +834,11 @@ nsWebBrowserFind::GetFrameSelection(nsPIDOMWindowOuter* aWindow)
nsCOMPtr<nsISelection> sel;
if (frame) {
frame->GetSelectionController(presContext, getter_AddRefs(selCon));
selCon->GetSelection(nsISelectionController::SELECTION_NORMAL,
getter_AddRefs(sel));
// JIHAD: same unguarded deref as above — GetSelectionController can yield null.
if (selCon) {
selCon->GetSelection(nsISelectionController::SELECTION_NORMAL,
getter_AddRefs(sel));
}
if (sel) {
int32_t count = -1;
sel->GetRangeCount(&count);
@@ -820,7 +848,12 @@ nsWebBrowserFind::GetFrameSelection(nsPIDOMWindowOuter* aWindow)
}
}
// JIHAD: and the fallback QI is not guaranteed either — if the presShell does not answer
// nsISelectionController we must return nothing rather than crash.
selCon = do_QueryInterface(presShell);
if (!selCon) {
return nullptr;
}
selCon->GetSelection(nsISelectionController::SELECTION_NORMAL,
getter_AddRefs(sel));
return sel.forget();
+2 -1
View File
@@ -12,5 +12,6 @@ if CONFIG['NS_PRINTING']:
DIRS += ['win']
elif toolkit == 'cocoa':
DIRS += ['mac']
elif CONFIG['MOZ_PDF_PRINTING']:
elif CONFIG['MOZ_PDF_PRINTING'] or toolkit == 'headless':
# Jihad headless: reuse the gtk-free PDF printing prompt service.
DIRS += ['unixshared']
+1 -1
View File
@@ -1374,7 +1374,7 @@ public:
static already_AddRefed<ScaledFont>
CreateScaledFontForNativeFont(const NativeFont &aNativeFont, Float aSize);
#ifdef MOZ_WIDGET_GTK
#if defined(MOZ_WIDGET_GTK) || defined(MOZ_WIDGET_HEADLESS)
static already_AddRefed<ScaledFont>
CreateScaledFontForFontconfigFont(cairo_scaled_font_t* aScaledFont, FcPattern* aPattern, Float aSize);
#endif
+2 -2
View File
@@ -30,7 +30,7 @@
#include "NativeFontResourceMac.h"
#endif
#ifdef MOZ_WIDGET_GTK
#if defined(MOZ_WIDGET_GTK) || defined(MOZ_WIDGET_HEADLESS)
#include "ScaledFontFontconfig.h"
#endif
@@ -584,7 +584,7 @@ Factory::CreateScaledFontWithCairo(const NativeFont& aNativeFont, Float aSize, c
#endif
}
#ifdef MOZ_WIDGET_GTK
#if defined(MOZ_WIDGET_GTK) || defined(MOZ_WIDGET_HEADLESS)
already_AddRefed<ScaledFont>
Factory::CreateScaledFontForFontconfigFont(cairo_scaled_font_t* aScaledFont, FcPattern* aPattern, Float aSize)
{
+3 -3
View File
@@ -91,7 +91,7 @@ if CONFIG['MOZ_WIDGET_TOOLKIT'] != 'windows':
'JobScheduler_posix.cpp',
]
if CONFIG['MOZ_WIDGET_TOOLKIT'] in ('gtk2', 'gtk3'):
if CONFIG['MOZ_WIDGET_TOOLKIT'] in ('gtk2', 'gtk3', 'headless'):
SOURCES += [
'ScaledFontFontconfig.cpp',
]
@@ -209,12 +209,12 @@ FINAL_LIBRARY = 'xul'
for var in ('USE_CAIRO', 'MOZ2D_HAS_MOZ_CAIRO'):
DEFINES[var] = True
if CONFIG['MOZ_WIDGET_TOOLKIT'] in ('gtk2', 'gtk3'):
if CONFIG['MOZ_WIDGET_TOOLKIT'] in ('gtk2', 'gtk3', 'headless'):
DEFINES['MOZ_ENABLE_FREETYPE'] = True
CXXFLAGS += CONFIG['MOZ_CAIRO_CFLAGS']
if CONFIG['MOZ_WIDGET_TOOLKIT'] in ('gtk2', 'gtk3'):
if CONFIG['MOZ_WIDGET_TOOLKIT'] in ('gtk2', 'gtk3', 'headless'):
CXXFLAGS += CONFIG['CAIRO_FT_CFLAGS']
LOCAL_INCLUDES += CONFIG['SKIA_INCLUDES']
+10 -3
View File
@@ -87,12 +87,19 @@ public:
bool
BasicPlanarYCbCrImage::CopyData(const Data& aData)
{
RecyclingPlanarYCbCrImage::CopyData(aData);
if (mDelayedConversion) {
if (!RecyclingPlanarYCbCrImage::CopyData(aData)) {
return false;
}
// The YCbCr copy above succeeded; delaying the RGB conversion is not a
// failure. GetAsSourceSurface() converts on demand when mDecodedBuffer is
// null. Returning false here made VideoData::CreateAndCopyData report OOM
// (MEDIA_ERR_DECODE) for any frame decoded while BasicImageLayer::Paint had
// swapped this factory into the video's ImageContainer.
if (mDelayedConversion) {
return true;
}
// Do some sanity checks to prevent integer overflow
if (aData.mYSize.width > PlanarYCbCrImage::MAX_DIMENSION ||
aData.mYSize.height > PlanarYCbCrImage::MAX_DIMENSION) {
+3 -2
View File
@@ -529,7 +529,7 @@ if CONFIG['MOZ_WIDGET_TOOLKIT'] in {'cocoa', 'uikit'}:
SOURCES += [
'skia/src/ports/SkFontHost_mac.cpp',
]
if 'gtk' in CONFIG['MOZ_WIDGET_TOOLKIT']:
if 'gtk' in CONFIG['MOZ_WIDGET_TOOLKIT'] or CONFIG['MOZ_WIDGET_TOOLKIT'] == 'headless':
UNIFIED_SOURCES += [
'skia/src/ports/SkDebug_stdio.cpp',
'skia/src/ports/SkOSFile_posix.cpp',
@@ -665,6 +665,7 @@ if CONFIG['MOZ_WIDGET_TOOLKIT'] in {
'cocoa',
'gtk2',
'gtk3',
'headless',
'uikit',
}:
DEFINES['SK_FONTHOST_DOES_NOT_USE_FONTMGR'] = 1
@@ -738,7 +739,7 @@ if CONFIG['CLANG_CXX'] or CONFIG['CLANG_CL']:
'-Wno-unused-private-field',
]
if CONFIG['MOZ_WIDGET_TOOLKIT'] in ('gtk2', 'gtk3'):
if CONFIG['MOZ_WIDGET_TOOLKIT'] in ('gtk2', 'gtk3', 'headless'):
CXXFLAGS += CONFIG['MOZ_CAIRO_CFLAGS']
CXXFLAGS += CONFIG['CAIRO_FT_CFLAGS']
+7
View File
@@ -32,6 +32,9 @@
#include <gdk/gdk.h>
#include "gfxPlatformGtk.h"
#endif
#ifdef MOZ_WIDGET_HEADLESS
#include "gfxPlatformHeadless.h"
#endif
#ifdef MOZ_X11
#include "mozilla/X11Util.h"
@@ -1764,7 +1767,11 @@ gfxFcPlatformFontList::FindGenericFamilies(const nsAString& aGeneric,
// -- select the fonts to be used for the generic
prefFonts = new PrefFontList; // can be empty but in practice won't happen
#ifdef MOZ_WIDGET_HEADLESS
uint32_t limit = gfxPlatformHeadless::GetPlatform()->MaxGenericSubstitions();
#else
uint32_t limit = gfxPlatformGtk::GetPlatform()->MaxGenericSubstitions();
#endif
bool foundFontWithLang = false;
for (int i = 0; i < faces->nfont; i++) {
FcPattern* font = faces->fonts[i];
+11
View File
@@ -40,6 +40,8 @@
#elif defined(XP_MACOSX)
#include "gfxPlatformMac.h"
#include "gfxQuartzSurface.h"
#elif defined(MOZ_WIDGET_HEADLESS)
#include "gfxPlatformHeadless.h"
#elif defined(MOZ_WIDGET_GTK)
#include "gfxPlatformGtk.h"
#elif defined(ANDROID)
@@ -572,6 +574,8 @@ gfxPlatform::Init()
gPlatform = new gfxWindowsPlatform;
#elif defined(XP_MACOSX)
gPlatform = new gfxPlatformMac;
#elif defined(MOZ_WIDGET_HEADLESS)
gPlatform = new gfxPlatformHeadless;
#elif defined(MOZ_WIDGET_GTK)
gPlatform = new gfxPlatformGtk;
#elif defined(ANDROID)
@@ -2166,6 +2170,13 @@ gfxPlatform::UsesOffMainThreadCompositing()
result |= gfxPrefs::LayersAccelerationForceEnabledDoNotUseDirectly();
#endif
// Jihad Browser: the headless BrowserServer embedder has no compositor
// process; allow forcing off-main-thread compositing off (use the in-process
// BasicLayerManager) via env, since the gfxPrefs "Once" snapshot can miss a
// grepref set during XRE_InitEmbedding2 init ordering.
if (getenv("JIHAD_DISABLE_OMTC")) {
result = false;
}
firstTime = false;
}
+15 -3
View File
@@ -72,7 +72,17 @@ static cairo_user_data_key_t cairo_gdk_drawable_key;
gfxPlatformGtk::gfxPlatformGtk()
{
gtk_init(nullptr, nullptr);
// Jihad: in JIHAD_OFFSCREEN (headless daemon) mode there is no X server on the
// device. gtk_init() calls exit() if it cannot open a display; gtk_init_check()
// returns false instead and leaves GTK usable for the non-X paths (fontconfig/
// freetype font list, image/CPU surfaces via the BasicLayerManager). The X code
// below is already guarded by GDK_IS_X11_DISPLAY(gdk_display_get_default()),
// which is false with no display, so it is skipped cleanly.
if (getenv("JIHAD_OFFSCREEN")) {
gtk_init_check(nullptr, nullptr);
} else {
gtk_init(nullptr, nullptr);
}
mMaxGenericSubstitutions = UNINITIALIZED_VALUE;
@@ -325,8 +335,10 @@ gfxPlatformGtk::GetDPI()
if (!sDPI) {
// Make sure init is run so we have a resolution
GdkScreen *screen = gdk_screen_get_default();
gtk_settings_get_for_screen(screen);
sDPI = int32_t(round(gdk_screen_get_resolution(screen)));
if (screen) { // null when headless (no X display)
gtk_settings_get_for_screen(screen);
sDPI = int32_t(round(gdk_screen_get_resolution(screen)));
}
if (sDPI <= 0) {
// Fall back to something sane
sDPI = 96;
+834
View File
@@ -0,0 +1,834 @@
/* -*- Mode: C++; tab-width: 20; indent-tabs-mode: nil; c-basic-offset: 4 -*-
* 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/. */
#define PANGO_ENABLE_BACKEND
#define PANGO_ENABLE_ENGINE
#include "gfxPlatformHeadless.h"
#include "prenv.h"
#include "nsUnicharUtils.h"
#include "nsUnicodeProperties.h"
#include "gfx2DGlue.h"
#include "gfxFcPlatformFontList.h"
#include "gfxConfig.h"
#include "gfxContext.h"
#include "gfxUserFontSet.h"
#include "gfxUtils.h"
#include "gfxFT2FontBase.h"
#include "gfxPrefs.h"
#include "gfxTextRun.h"
#include "VsyncSource.h"
#include "mozilla/Atomics.h"
#include "mozilla/Monitor.h"
#include "base/task.h"
#include "base/thread.h"
#include "base/message_loop.h"
#include "mozilla/gfx/Logging.h"
#include "mozilla/gfx/2D.h"
#include "cairo.h"
#include "gfxImageSurface.h"
#include "mozilla/Preferences.h"
#ifdef MOZ_X11
#include "gfxXlibSurface.h"
#include "cairo-xlib.h"
#include "mozilla/X11Util.h"
#ifdef GL_PROVIDER_GLX
#include "GLContextProvider.h"
#include "GLContextGLX.h"
#include "GLXLibrary.h"
#endif
/* Undefine the Status from Xlib since it will conflict with system headers on OSX */
#if defined(__APPLE__) && defined(Status)
#undef Status
#endif
#endif /* MOZ_X11 */
#include <fontconfig/fontconfig.h>
#include "nsMathUtils.h"
#define GDK_PIXMAP_SIZE_MAX 32767
#define GFX_PREF_MAX_GENERIC_SUBSTITUTIONS "gfx.font_rendering.fontconfig.max_generic_substitutions"
using namespace mozilla;
using namespace mozilla::gfx;
using namespace mozilla::unicode;
#if (MOZ_WIDGET_GTK == 2)
static cairo_user_data_key_t cairo_gdk_drawable_key;
#endif
gfxPlatformHeadless::gfxPlatformHeadless()
{
// Jihad: in JIHAD_OFFSCREEN (headless daemon) mode there is no X server on the
// device. gtk_init() calls exit() if it cannot open a display; gtk_init_check()
// returns false instead and leaves GTK usable for the non-X paths (fontconfig/
// freetype font list, image/CPU surfaces via the BasicLayerManager). The X code
// below is already guarded by GDK_IS_X11_DISPLAY(gdk_display_get_default()),
// which is false with no display, so it is skipped cleanly.
/* headless: no gtk_init */
mMaxGenericSubstitutions = UNINITIALIZED_VALUE;
#ifdef MOZ_X11
if (XRE_IsParentProcess()) {
if (GDK_IS_X11_DISPLAY(gdk_display_get_default()) &&
mozilla::Preferences::GetBool("gfx.xrender.enabled"))
{
gfxVars::SetUseXRender(true);
}
}
#endif
uint32_t canvasMask = BackendTypeBit(BackendType::CAIRO);
uint32_t contentMask = BackendTypeBit(BackendType::CAIRO);
#ifdef USE_SKIA
canvasMask |= BackendTypeBit(BackendType::SKIA);
contentMask |= BackendTypeBit(BackendType::SKIA);
#endif
InitBackendPrefs(canvasMask, BackendType::CAIRO,
contentMask, BackendType::CAIRO);
#ifdef MOZ_X11
if (GDK_IS_X11_DISPLAY(gdk_display_get_default())) {
mCompositorDisplay = XOpenDisplay(nullptr);
MOZ_ASSERT(mCompositorDisplay, "Failed to create compositor display!");
} else {
mCompositorDisplay = nullptr;
}
#endif // MOZ_X11
}
gfxPlatformHeadless::~gfxPlatformHeadless()
{
#ifdef MOZ_X11
if (mCompositorDisplay) {
XCloseDisplay(mCompositorDisplay);
}
#endif // MOZ_X11
}
void
gfxPlatformHeadless::FlushContentDrawing()
{
#ifdef MOZ_X11
if (gfxVars::UseXRender()) {
XFlush(DefaultXDisplay());
}
#endif
}
already_AddRefed<gfxASurface>
gfxPlatformHeadless::CreateOffscreenSurface(const IntSize& aSize,
gfxImageFormat aFormat)
{
if (!Factory::AllowedSurfaceSize(aSize)) {
return nullptr;
}
RefPtr<gfxASurface> newSurface;
bool needsClear = true;
#ifdef MOZ_X11
// XXX we really need a different interface here, something that passes
// in more context, including the display and/or target surface type that
// we should try to match
GdkScreen *gdkScreen = gdk_screen_get_default();
if (gdkScreen) {
// When forcing PaintedLayers to use image surfaces for content,
// force creation of gfxImageSurface surfaces.
if (gfxVars::UseXRender()) {
Screen *screen = gdk_x11_screen_get_xscreen(gdkScreen);
XRenderPictFormat* xrenderFormat =
gfxXlibSurface::FindRenderFormat(DisplayOfScreen(screen),
aFormat);
if (xrenderFormat) {
newSurface = gfxXlibSurface::Create(screen, xrenderFormat,
aSize);
}
}
}
#endif
if (!newSurface) {
// We couldn't create a native surface for whatever reason;
// e.g., no display, no RENDER, bad size, etc.
// Fall back to image surface for the data.
newSurface = new gfxImageSurface(aSize, aFormat);
// The gfxImageSurface ctor zeroes this for us, no need to
// waste time clearing again
needsClear = false;
}
if (newSurface->CairoStatus()) {
newSurface = nullptr; // surface isn't valid for some reason
}
if (newSurface && needsClear) {
gfxUtils::ClearThebesSurface(newSurface);
}
return newSurface.forget();
}
nsresult
gfxPlatformHeadless::GetFontList(nsIAtom *aLangGroup,
const nsACString& aGenericFamily,
nsTArray<nsString>& aListOfFonts)
{
gfxPlatformFontList::PlatformFontList()->GetFontList(aLangGroup,
aGenericFamily,
aListOfFonts);
return NS_OK;
}
nsresult
gfxPlatformHeadless::UpdateFontList()
{
gfxPlatformFontList::PlatformFontList()->UpdateFontList();
return NS_OK;
}
// xxx - this is ubuntu centric, need to go through other distros and flesh
// out a more general list
static const char kFontDejaVuSans[] = "DejaVu Sans";
static const char kFontDejaVuSerif[] = "DejaVu Serif";
static const char kFontFreeSans[] = "FreeSans";
static const char kFontFreeSerif[] = "FreeSerif";
static const char kFontTakaoPGothic[] = "TakaoPGothic";
static const char kFontTwemojiMozilla[] = "Twemoji Mozilla";
static const char kFontDroidSansFallback[] = "Droid Sans Fallback";
static const char kFontWenQuanYiMicroHei[] = "WenQuanYi Micro Hei";
static const char kFontNanumGothic[] = "NanumGothic";
void
gfxPlatformHeadless::GetCommonFallbackFonts(uint32_t aCh, uint32_t aNextCh,
Script aRunScript,
nsTArray<const char*>& aFontList)
{
EmojiPresentation emoji = GetEmojiPresentation(aCh);
EmojiPresentation eNext = GetEmojiPresentation(aNextCh, true);
if (aNextCh != kVariationSelector15 &&
emoji != EmojiPresentation::TextOnly &&
(emoji != EmojiPresentation::TextDefault ||
eNext == EmojiPresentation::EmojiComponent)) {
aFontList.AppendElement(kFontTwemojiMozilla);
}
aFontList.AppendElement(kFontDejaVuSerif);
aFontList.AppendElement(kFontFreeSerif);
aFontList.AppendElement(kFontDejaVuSans);
aFontList.AppendElement(kFontFreeSans);
// add fonts for CJK ranges
// xxx - this isn't really correct, should use the same CJK font ordering
// as the pref font code
if (aCh >= 0x3000 &&
((aCh < 0xe000) ||
(aCh >= 0xf900 && aCh < 0xfff0) ||
((aCh >> 16) == 2))) {
aFontList.AppendElement(kFontTakaoPGothic);
aFontList.AppendElement(kFontDroidSansFallback);
aFontList.AppendElement(kFontWenQuanYiMicroHei);
aFontList.AppendElement(kFontNanumGothic);
}
}
gfxPlatformFontList*
gfxPlatformHeadless::CreatePlatformFontList()
{
gfxPlatformFontList* list = new gfxFcPlatformFontList();
if (NS_SUCCEEDED(list->InitFontList())) {
return list;
}
gfxPlatformFontList::Shutdown();
return nullptr;
}
nsresult
gfxPlatformHeadless::GetStandardFamilyName(const nsAString& aFontName, nsAString& aFamilyName)
{
gfxPlatformFontList::PlatformFontList()->
GetStandardFamilyName(aFontName, aFamilyName);
return NS_OK;
}
gfxFontGroup *
gfxPlatformHeadless::CreateFontGroup(const FontFamilyList& aFontFamilyList,
const gfxFontStyle* aStyle,
gfxTextPerfMetrics* aTextPerf,
gfxUserFontSet* aUserFontSet,
gfxFloat aDevToCssSize)
{
return new gfxFontGroup(aFontFamilyList, aStyle, aTextPerf,
aUserFontSet, aDevToCssSize);
}
gfxFontEntry*
gfxPlatformHeadless::LookupLocalFont(const nsAString& aFontName,
uint16_t aWeight,
int16_t aStretch,
uint8_t aStyle)
{
gfxPlatformFontList* pfl = gfxPlatformFontList::PlatformFontList();
return pfl->LookupLocalFont(aFontName, aWeight, aStretch,
aStyle);
}
gfxFontEntry*
gfxPlatformHeadless::MakePlatformFont(const nsAString& aFontName,
uint16_t aWeight,
int16_t aStretch,
uint8_t aStyle,
const uint8_t* aFontData,
uint32_t aLength)
{
gfxPlatformFontList* pfl = gfxPlatformFontList::PlatformFontList();
return pfl->MakePlatformFont(aFontName, aWeight, aStretch,
aStyle, aFontData, aLength);
}
bool
gfxPlatformHeadless::IsFontFormatSupported(nsIURI *aFontURI, uint32_t aFormatFlags)
{
// check for strange format flags
NS_ASSERTION(!(aFormatFlags & gfxUserFontSet::FLAG_FORMAT_NOT_USED),
"strange font format hint set");
// accept supported formats
// Pango doesn't apply features from AAT TrueType extensions.
// Assume that if this is the only SFNT format specified,
// then AAT extensions are required for complex script support.
if (aFormatFlags & gfxUserFontSet::FLAG_FORMATS_COMMON) {
return true;
}
// reject all other formats, known and unknown
if (aFormatFlags != 0) {
return false;
}
// no format hint set, need to look at data
return true;
}
static int32_t sDPI = 0;
int32_t
gfxPlatformHeadless::GetDPI()
{
if (!sDPI) {
// Make sure init is run so we have a resolution
sDPI = 96;
if (sDPI <= 0) {
// Fall back to something sane
sDPI = 96;
}
}
return sDPI;
}
double
gfxPlatformHeadless::GetDPIScale()
{
// Integer scale factors work well with GTK window scaling, image scaling,
// and pixel alignment, but there is a range where 1 is too small and 2 is
// too big. An additional step of 1.5 is added because this is common
// scale on WINNT and at this ratio the advantages of larger rendering
// outweigh the disadvantages from scaling and pixel mis-alignment.
int32_t dpi = GetDPI();
if (dpi < 144) {
return 1.0;
} else if (dpi < 168) {
return 1.5;
} else {
return round(dpi/96.0);
}
}
bool
gfxPlatformHeadless::UseImageOffscreenSurfaces()
{
return GetDefaultContentBackend() != mozilla::gfx::BackendType::CAIRO ||
gfxPrefs::UseImageOffscreenSurfaces();
}
gfxImageFormat
gfxPlatformHeadless::GetOffscreenFormat()
{
// Make sure there is a screen
return SurfaceFormat::X8R8G8B8_UINT32;
}
void gfxPlatformHeadless::FontsPrefsChanged(const char *aPref)
{
// only checking for generic substitions, pass other changes up
if (strcmp(GFX_PREF_MAX_GENERIC_SUBSTITUTIONS, aPref)) {
gfxPlatform::FontsPrefsChanged(aPref);
return;
}
mMaxGenericSubstitutions = UNINITIALIZED_VALUE;
gfxFcPlatformFontList* pfl = gfxFcPlatformFontList::PlatformFontList();
pfl->ClearGenericMappings();
FlushFontAndWordCaches();
}
uint32_t gfxPlatformHeadless::MaxGenericSubstitions()
{
if (mMaxGenericSubstitutions == UNINITIALIZED_VALUE) {
mMaxGenericSubstitutions =
Preferences::GetInt(GFX_PREF_MAX_GENERIC_SUBSTITUTIONS, 3);
if (mMaxGenericSubstitutions < 0) {
mMaxGenericSubstitutions = 3;
}
}
return uint32_t(mMaxGenericSubstitutions);
}
void
gfxPlatformHeadless::GetPlatformCMSOutputProfile(void *&mem, size_t &size)
{
mem = nullptr;
size = 0;
#ifdef MOZ_X11
GdkDisplay *display = gdk_display_get_default();
if (!GDK_IS_X11_DISPLAY(display))
return;
const char EDID1_ATOM_NAME[] = "XFree86_DDC_EDID1_RAWDATA";
const char ICC_PROFILE_ATOM_NAME[] = "_ICC_PROFILE";
Atom edidAtom, iccAtom;
Display *dpy = GDK_DISPLAY_XDISPLAY(display);
// In xpcshell tests, we never initialize X and hence don't have a Display.
// In this case, there's no output colour management to be done, so we just
// return with nullptr.
if (!dpy)
return;
Window root = gdk_x11_get_default_root_xwindow();
Atom retAtom;
int retFormat;
unsigned long retLength, retAfter;
unsigned char *retProperty ;
iccAtom = XInternAtom(dpy, ICC_PROFILE_ATOM_NAME, TRUE);
if (iccAtom) {
// read once to get size, once for the data
if (Success == XGetWindowProperty(dpy, root, iccAtom,
0, INT_MAX /* length */,
False, AnyPropertyType,
&retAtom, &retFormat, &retLength,
&retAfter, &retProperty)) {
if (retLength > 0) {
void *buffer = malloc(retLength);
if (buffer) {
memcpy(buffer, retProperty, retLength);
mem = buffer;
size = retLength;
}
}
XFree(retProperty);
if (size > 0) {
#ifdef DEBUG_tor
fprintf(stderr,
"ICM profile read from %s successfully\n",
ICC_PROFILE_ATOM_NAME);
#endif
return;
}
}
}
edidAtom = XInternAtom(dpy, EDID1_ATOM_NAME, TRUE);
if (edidAtom) {
if (Success == XGetWindowProperty(dpy, root, edidAtom, 0, 32,
False, AnyPropertyType,
&retAtom, &retFormat, &retLength,
&retAfter, &retProperty)) {
double gamma;
qcms_CIE_xyY whitePoint;
qcms_CIE_xyYTRIPLE primaries;
if (retLength != 128) {
#ifdef DEBUG_tor
fprintf(stderr, "Short EDID data\n");
#endif
return;
}
// Format documented in "VESA E-EDID Implementation Guide"
gamma = (100 + retProperty[0x17]) / 100.0;
whitePoint.x = ((retProperty[0x21] << 2) |
(retProperty[0x1a] >> 2 & 3)) / 1024.0;
whitePoint.y = ((retProperty[0x22] << 2) |
(retProperty[0x1a] >> 0 & 3)) / 1024.0;
whitePoint.Y = 1.0;
primaries.red.x = ((retProperty[0x1b] << 2) |
(retProperty[0x19] >> 6 & 3)) / 1024.0;
primaries.red.y = ((retProperty[0x1c] << 2) |
(retProperty[0x19] >> 4 & 3)) / 1024.0;
primaries.red.Y = 1.0;
primaries.green.x = ((retProperty[0x1d] << 2) |
(retProperty[0x19] >> 2 & 3)) / 1024.0;
primaries.green.y = ((retProperty[0x1e] << 2) |
(retProperty[0x19] >> 0 & 3)) / 1024.0;
primaries.green.Y = 1.0;
primaries.blue.x = ((retProperty[0x1f] << 2) |
(retProperty[0x1a] >> 6 & 3)) / 1024.0;
primaries.blue.y = ((retProperty[0x20] << 2) |
(retProperty[0x1a] >> 4 & 3)) / 1024.0;
primaries.blue.Y = 1.0;
XFree(retProperty);
#ifdef DEBUG_tor
fprintf(stderr, "EDID gamma: %f\n", gamma);
fprintf(stderr, "EDID whitepoint: %f %f %f\n",
whitePoint.x, whitePoint.y, whitePoint.Y);
fprintf(stderr, "EDID primaries: [%f %f %f] [%f %f %f] [%f %f %f]\n",
primaries.Red.x, primaries.Red.y, primaries.Red.Y,
primaries.Green.x, primaries.Green.y, primaries.Green.Y,
primaries.Blue.x, primaries.Blue.y, primaries.Blue.Y);
#endif
qcms_data_create_rgb_with_gamma(whitePoint, primaries, gamma, &mem, &size);
#ifdef DEBUG_tor
if (size > 0) {
fprintf(stderr,
"ICM profile read from %s successfully\n",
EDID1_ATOM_NAME);
}
#endif
}
}
#endif
}
#if (MOZ_WIDGET_GTK == 2)
void
gfxPlatformHeadless::SetGdkDrawable(cairo_surface_t *target,
GdkDrawable *drawable)
{
if (cairo_surface_status(target))
return;
g_object_ref(drawable);
cairo_surface_set_user_data (target,
&cairo_gdk_drawable_key,
drawable,
g_object_unref);
}
GdkDrawable *
gfxPlatformHeadless::GetGdkDrawable(cairo_surface_t *target)
{
if (cairo_surface_status(target))
return nullptr;
GdkDrawable *result;
result = (GdkDrawable*) cairo_surface_get_user_data (target,
&cairo_gdk_drawable_key);
if (result)
return result;
#ifdef MOZ_X11
if (cairo_surface_get_type(target) != CAIRO_SURFACE_TYPE_XLIB)
return nullptr;
// try looking it up in gdk's table
result = (GdkDrawable*) gdk_xid_table_lookup(cairo_xlib_surface_get_drawable(target));
if (result) {
SetGdkDrawable(target, result);
return result;
}
#endif
return nullptr;
}
#endif
#ifdef GL_PROVIDER_GLX
class GLXVsyncSource final : public VsyncSource
{
public:
GLXVsyncSource()
{
MOZ_ASSERT(NS_IsMainThread());
mGlobalDisplay = new GLXDisplay();
}
virtual ~GLXVsyncSource()
{
MOZ_ASSERT(NS_IsMainThread());
}
virtual Display& GetGlobalDisplay() override
{
return *mGlobalDisplay;
}
class GLXDisplay final : public VsyncSource::Display
{
NS_INLINE_DECL_THREADSAFE_REFCOUNTING(GLXDisplay)
public:
GLXDisplay() : mGLContext(nullptr)
, mXDisplay(nullptr)
, mSetupLock("GLXVsyncSetupLock")
, mVsyncThread("GLXVsyncThread")
, mVsyncTask(nullptr)
, mVsyncEnabledLock("GLXVsyncEnabledLock")
, mVsyncEnabled(false)
{
}
// Sets up the display's GL context on a worker thread.
// Required as GLContexts may only be used by the creating thread.
// Returns true if setup was a success.
bool Setup()
{
MonitorAutoLock lock(mSetupLock);
MOZ_ASSERT(NS_IsMainThread());
if (!mVsyncThread.Start())
return false;
RefPtr<Runnable> vsyncSetup = NewRunnableMethod(this, &GLXDisplay::SetupGLContext);
mVsyncThread.message_loop()->PostTask(vsyncSetup.forget());
// Wait until the setup has completed.
lock.Wait();
return mGLContext != nullptr;
}
// Called on the Vsync thread to setup the GL context.
void SetupGLContext()
{
MonitorAutoLock lock(mSetupLock);
MOZ_ASSERT(!NS_IsMainThread());
MOZ_ASSERT(!mGLContext, "GLContext already setup!");
// Create video sync timer on a separate Display to prevent locking the
// main thread X display.
mXDisplay = XOpenDisplay(nullptr);
if (!mXDisplay) {
lock.NotifyAll();
return;
}
// Most compositors wait for vsync events on the root window.
Window root = DefaultRootWindow(mXDisplay);
int screen = DefaultScreen(mXDisplay);
ScopedXFree<GLXFBConfig> cfgs;
GLXFBConfig config;
int visid;
if (!gl::GLContextGLX::FindFBConfigForWindow(mXDisplay, screen, root,
&cfgs, &config, &visid)) {
lock.NotifyAll();
return;
}
mGLContext = gl::GLContextGLX::CreateGLContext(
gl::CreateContextFlags::NONE,
gl::SurfaceCaps::Any(),
nullptr,
false,
mXDisplay,
root,
config);
if (!mGLContext) {
lock.NotifyAll();
return;
}
mGLContext->MakeCurrent();
// Test that SGI_video_sync lets us get the counter.
unsigned int syncCounter = 0;
if (gl::sGLXLibrary.xGetVideoSync(&syncCounter) != 0) {
mGLContext = nullptr;
}
lock.NotifyAll();
}
virtual void EnableVsync() override
{
MOZ_ASSERT(NS_IsMainThread());
MOZ_ASSERT(mGLContext, "GLContext not setup!");
MonitorAutoLock lock(mVsyncEnabledLock);
if (mVsyncEnabled) {
return;
}
mVsyncEnabled = true;
// If the task has not nulled itself out, it hasn't yet realized
// that vsync was disabled earlier, so continue its execution.
if (!mVsyncTask) {
mVsyncTask = NewRunnableMethod(this, &GLXDisplay::RunVsync);
RefPtr<Runnable> addrefedTask = mVsyncTask;
mVsyncThread.message_loop()->PostTask(addrefedTask.forget());
}
}
virtual void DisableVsync() override
{
MonitorAutoLock lock(mVsyncEnabledLock);
mVsyncEnabled = false;
}
virtual bool IsVsyncEnabled() override
{
MonitorAutoLock lock(mVsyncEnabledLock);
return mVsyncEnabled;
}
virtual void Shutdown() override
{
MOZ_ASSERT(NS_IsMainThread());
DisableVsync();
// Cleanup thread-specific resources before shutting down.
RefPtr<Runnable> shutdownTask = NewRunnableMethod(this, &GLXDisplay::Cleanup);
mVsyncThread.message_loop()->PostTask(shutdownTask.forget());
// Stop, waiting for the cleanup task to finish execution.
mVsyncThread.Stop();
}
private:
virtual ~GLXDisplay()
{
}
void RunVsync()
{
MOZ_ASSERT(!NS_IsMainThread());
mGLContext->MakeCurrent();
unsigned int syncCounter = 0;
gl::sGLXLibrary.xGetVideoSync(&syncCounter);
for (;;) {
{
MonitorAutoLock lock(mVsyncEnabledLock);
if (!mVsyncEnabled) {
mVsyncTask = nullptr;
return;
}
}
TimeStamp lastVsync = TimeStamp::Now();
bool useSoftware = false;
// Wait until the video sync counter reaches the next value by waiting
// until the parity of the counter value changes.
unsigned int nextSync = syncCounter + 1;
int status;
if ((status = gl::sGLXLibrary.xWaitVideoSync(2, nextSync % 2, &syncCounter)) != 0) {
gfxWarningOnce() << "glXWaitVideoSync returned " << status;
useSoftware = true;
}
if (syncCounter == (nextSync - 1)) {
gfxWarningOnce() << "glXWaitVideoSync failed to increment the sync counter.";
useSoftware = true;
}
if (useSoftware) {
double remaining = (1000.f / 60.f) -
(TimeStamp::Now() - lastVsync).ToMilliseconds();
if (remaining > 0) {
PlatformThread::Sleep(remaining);
}
}
lastVsync = TimeStamp::Now();
NotifyVsync(lastVsync);
}
}
void Cleanup() {
MOZ_ASSERT(!NS_IsMainThread());
mGLContext = nullptr;
XCloseDisplay(mXDisplay);
}
// Owned by the vsync thread.
RefPtr<gl::GLContextGLX> mGLContext;
_XDisplay* mXDisplay;
Monitor mSetupLock;
base::Thread mVsyncThread;
RefPtr<Runnable> mVsyncTask;
Monitor mVsyncEnabledLock;
bool mVsyncEnabled;
};
private:
// We need a refcounted VsyncSource::Display to use chromium IPC runnables.
RefPtr<GLXDisplay> mGlobalDisplay;
};
already_AddRefed<gfx::VsyncSource>
gfxPlatformHeadless::CreateHardwareVsyncSource()
{
// Only use GLX vsync when the OpenGL compositor is being used.
// The extra cost of initializing a GLX context while blocking the main
// thread is not worth it when using basic composition.
if (gfxConfig::IsEnabled(Feature::HW_COMPOSITING)) {
if (gl::sGLXLibrary.SupportsVideoSync()) {
RefPtr<VsyncSource> vsyncSource = new GLXVsyncSource();
VsyncSource::Display& display = vsyncSource->GetGlobalDisplay();
if (!static_cast<GLXVsyncSource::GLXDisplay&>(display).Setup()) {
NS_WARNING("Failed to setup GLContext, falling back to software vsync.");
return gfxPlatform::CreateHardwareVsyncSource();
}
return vsyncSource.forget();
}
NS_WARNING("SGI_video_sync unsupported. Falling back to software vsync.");
}
return gfxPlatform::CreateHardwareVsyncSource();
}
#endif
// Not GLX-specific: must be defined regardless of GL_PROVIDER_GLX so the
// vtable slot resolves in a headless (no X/GLX) build.
bool
gfxPlatformHeadless::SupportsApzTouchInput() const
{
int value = gfxPrefs::TouchEventsEnabled();
return value == 1 || value == 2;
}
+150
View File
@@ -0,0 +1,150 @@
/* -*- Mode: C++; tab-width: 20; indent-tabs-mode: nil; c-basic-offset: 4 -*-
* 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/. */
#ifndef GFX_PLATFORM_GTK_H
#define GFX_PLATFORM_GTK_H
#include "gfxPlatform.h"
#include "nsAutoRef.h"
#include "nsTArray.h"
#include "mozilla/gfx/gfxVars.h"
#if (MOZ_WIDGET_GTK == 2)
extern "C" {
typedef struct _GdkDrawable GdkDrawable;
}
#endif
#ifdef MOZ_X11
struct _XDisplay;
typedef struct _XDisplay Display;
#endif // MOZ_X11
class gfxPlatformHeadless : public gfxPlatform {
public:
gfxPlatformHeadless();
virtual ~gfxPlatformHeadless();
static gfxPlatformHeadless *GetPlatform() {
return (gfxPlatformHeadless*) gfxPlatform::GetPlatform();
}
virtual already_AddRefed<gfxASurface>
CreateOffscreenSurface(const IntSize& aSize,
gfxImageFormat aFormat) override;
virtual nsresult GetFontList(nsIAtom *aLangGroup,
const nsACString& aGenericFamily,
nsTArray<nsString>& aListOfFonts) override;
virtual nsresult UpdateFontList() override;
virtual void
GetCommonFallbackFonts(uint32_t aCh, uint32_t aNextCh,
Script aRunScript,
nsTArray<const char*>& aFontList) override;
virtual gfxPlatformFontList* CreatePlatformFontList() override;
virtual nsresult GetStandardFamilyName(const nsAString& aFontName,
nsAString& aFamilyName) override;
gfxFontGroup*
CreateFontGroup(const mozilla::FontFamilyList& aFontFamilyList,
const gfxFontStyle *aStyle,
gfxTextPerfMetrics* aTextPerf,
gfxUserFontSet *aUserFontSet,
gfxFloat aDevToCssSize) override;
/**
* Look up a local platform font using the full font face name (needed to
* support @font-face src local() )
*/
virtual gfxFontEntry* LookupLocalFont(const nsAString& aFontName,
uint16_t aWeight,
int16_t aStretch,
uint8_t aStyle) override;
/**
* Activate a platform font (needed to support @font-face src url() )
*
*/
virtual gfxFontEntry* MakePlatformFont(const nsAString& aFontName,
uint16_t aWeight,
int16_t aStretch,
uint8_t aStyle,
const uint8_t* aFontData,
uint32_t aLength) override;
/**
* Check whether format is supported on a platform or not (if unclear,
* returns true).
*/
virtual bool IsFontFormatSupported(nsIURI *aFontURI,
uint32_t aFormatFlags) override;
/**
* Calls XFlush if xrender is enabled.
*/
virtual void FlushContentDrawing() override;
#if (MOZ_WIDGET_GTK == 2)
static void SetGdkDrawable(cairo_surface_t *target,
GdkDrawable *drawable);
static GdkDrawable *GetGdkDrawable(cairo_surface_t *target);
#endif
static int32_t GetDPI();
static double GetDPIScale();
#ifdef MOZ_X11
virtual void GetAzureBackendInfo(mozilla::widget::InfoObject &aObj) override {
gfxPlatform::GetAzureBackendInfo(aObj);
aObj.DefineProperty("CairoUseXRender", mozilla::gfx::gfxVars::UseXRender());
}
#endif
bool UseImageOffscreenSurfaces();
virtual gfxImageFormat GetOffscreenFormat() override;
bool SupportsApzWheelInput() const override {
return true;
}
bool SupportsApzTouchInput() const override;
void FontsPrefsChanged(const char *aPref) override;
// maximum number of fonts to substitute for a generic
uint32_t MaxGenericSubstitions();
bool SupportsPluginDirectBitmapDrawing() override {
return true;
}
#ifdef GL_PROVIDER_GLX
already_AddRefed<mozilla::gfx::VsyncSource> CreateHardwareVsyncSource() override;
#endif
#ifdef MOZ_X11
Display* GetCompositorDisplay() {
return mCompositorDisplay;
}
#endif // MOZ_X11
protected:
int8_t mMaxGenericSubstitutions;
private:
virtual void GetPlatformCMSOutputProfile(void *&mem,
size_t &size) override;
#ifdef MOZ_X11
Display* mCompositorDisplay;
#endif
};
#endif /* GFX_PLATFORM_GTK_H */
+16
View File
@@ -74,6 +74,22 @@ if CONFIG['MOZ_WIDGET_TOOLKIT'] == 'cocoa':
'gfxQuartzSurface.cpp',
'PrintTargetCG.cpp',
]
elif CONFIG['MOZ_WIDGET_TOOLKIT'] == 'headless':
# Jihad headless (no X/GTK): fontconfig/freetype fonts + a memory gfxPlatform.
EXPORTS += [
'gfxFT2FontBase.h',
'gfxPlatformHeadless.h',
]
EXPORTS.mozilla.gfx += [
'PrintTargetPDF.h',
]
SOURCES += [
'gfxFcPlatformFontList.cpp',
'gfxFT2FontBase.cpp',
'gfxFT2Utils.cpp',
'gfxPlatformHeadless.cpp',
'PrintTargetPDF.cpp',
]
elif 'gtk' in CONFIG['MOZ_WIDGET_TOOLKIT']:
EXPORTS += [
'gfxFT2FontBase.h',
+8
View File
@@ -6,6 +6,14 @@
GeckoProgram(CONFIG['MOZ_CHILD_PROCESS_NAME'], linkage='dependent')
SOURCES += [
# JihadPmLogSem.c interposes sem_open/sem_close for the ONE name "PmLogLib". The device's
# Flash pulls in libPmLogLib.so, whose constructor sem_wait()s a named semaphore created
# by webOS's glibc 2.8; glibc 2.21 changed sem_t's value word to (tokens << 1), so our
# bundled 2.23 reads that semaphore as "no tokens, waiters pending" and the dlopen never
# returns. It has to be in THIS directory: a dlopened plugin resolves its PLT against the
# global scope, which the program image heads, and ipc/app is where the program is linked
# (ipc/contentproc only produces an archive this link does not use).
'../contentproc/JihadPmLogSem.c',
'MozillaRuntimeMain.cpp',
]
+6
View File
@@ -146,7 +146,13 @@ MessageLoop::MessageLoop(Type type, nsIThread* aThread)
#if defined(OS_MACOSX)
pump_ = base::MessagePumpMac::Create();
#elif defined(OS_LINUX) || defined(OS_BSD) || defined(OS_SOLARIS)
#if defined(MOZ_WIDGET_GTK)
pump_ = new base::MessagePumpForUI();
#else
// Jihad headless: no GTK UI pump; the embedder drives events via
// NS_ProcessNextEvent, so a default (non-UI) message pump suffices.
pump_ = new base::MessagePumpDefault();
#endif
#endif // OS_LINUX
} else if (type_ == TYPE_IO) {
pump_ = new base::MessagePumpLibevent();
+135
View File
@@ -0,0 +1,135 @@
/* 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 https://mozilla.org/MPL/2.0/.
*
* Jihad Browser — the /PmLogLib named-semaphore ABI trap.
*
* Compiled into every one of our binaries that can dlopen a device NPAPI plugin: the
* daemon (its plugin scan) and plugin-container (the plugin itself). Plain C, no headers
* from UXP or from the daemon, so the same file drops into both build systems unchanged.
*/
// ── WHAT THIS IS FOR ────────────────────────────────────────────────────────────────────
//
// The device's libflashplayer.so has libWebKitLuna.so as a DT_NEEDED, which drags in
// libPmLogLib.so. PmLogLib's ELF constructor opens the POSIX named semaphore "PmLogLib"
// and sem_wait()s it to guard a SysV shared-memory table of log contexts that every
// process on the device shares.
//
// glibc changed sem_t's layout in 2.21. The value word went from a raw token count to
// (tokens << 1) | has-waiters. webOS 3's userland is glibc 2.8 — the OLD layout — and
// everything we cross-build runs on the bundled glibc 2.23, the NEW one. /dev/shm/
// sem.PmLogLib is created once per boot by whichever process opens it first, in that
// process's layout, and then outlives every user of it.
//
// When a system process wins that race the word holds a raw 1, meaning "one token". Our
// 2.23 sem_wait reads the same word as ZERO tokens with the waiters bit set, so it goes
// straight to futex_wait and never returns. dlopen never completes and whoever called it
// wedges: measured on device 2026-08-10 as
// futex(0x2dd98000, FUTEX_WAIT_BITSET_PRIVATE|FUTEX_CLOCK_REALTIME, 1, NULL, ffffffff
// immediately after the mmap of /dev/shm/sem.PmLogLib, with every library already loaded.
//
// That is why this reads as an intermittent, reboot-triggered failure on binaries nobody
// touched: it is decided by a boot race, not by our code. The same dlopen run under the
// device's own /lib/ld-linux.so.3 completes and returns both Flash MIME types.
//
// ── WHY INTERPOSE RATHER THAN REPAIR THE SEMAPHORE ──────────────────────────────────────
//
// Rewriting /dev/shm/sem.PmLogLib into our layout, or posting it once to make it readable
// to us, both work — and both hand every OTHER process on the device an extra token on a
// lock guarding a shared table. That silently weakens PmLogLib's mutual exclusion
// system-wide to fix a browser. Interposing ONE name does not: PmLogLib gets a semaphore
// that never leaves this process, so no other glibc ever parses its layout, and the file
// in /dev/shm is not touched at all.
//
// Everything else is forwarded to the real implementation untouched, which MATTERS: our
// own shared frame buffers are named semaphores too (/dev/shm/sem.browserserver.<pid>-<n>,
// created here and opened by the adapter inside LunaSysMgr), and those genuinely must stay
// shared across the process boundary.
#define _GNU_SOURCE
#include <dlfcn.h>
#include <fcntl.h>
#include <pthread.h>
#include <semaphore.h>
#include <stdarg.h>
#include <stddef.h>
#include <string.h>
// PmLogLib asks for the name WITHOUT a leading slash (verified on device: the interposer
// logged sem_open("PmLogLib")). POSIX says a portable name starts with '/', and glibc
// accepts both, so match both rather than trusting either spelling.
// Link anchor. plugin-container builds this TU into a static archive, and an archive member
// is only pulled in to satisfy an UNDEFINED symbol — sem_open is already defined by
// libpthread, so nothing was undefined and the whole file was silently dropped from the
// link. Calling this from the program's entry point forces the member in. The daemon links
// the object directly and does not need it; it costs one call there.
__attribute__((visibility("default")))
int jihad_pmlog_sem_installed(void) { return 1; }
static int jihad_is_pmlog_name(const char* name)
{
if (!name) return 0;
if (name[0] == '/') ++name;
return strcmp(name, "PmLogLib") == 0;
}
static sem_t g_pmlogSem;
static pthread_once_t g_pmlogSemOnce = PTHREAD_ONCE_INIT;
static unsigned int g_pmlogSemInitial = 1;
static void jihad_pmlog_sem_init(void)
{
// pshared = 0. Process-private is the whole point: it can never be observed, and so can
// never be misread, by a glibc with a different sem_t layout.
sem_init(&g_pmlogSem, 0, g_pmlogSemInitial);
}
__attribute__((visibility("default")))
sem_t* sem_open(const char* name, int oflag, ...)
{
static sem_t* (*real_sem_open)(const char*, int, ...);
if (!real_sem_open)
real_sem_open = (sem_t* (*)(const char*, int, ...))dlsym(RTLD_NEXT, "sem_open");
// The mode/value pair is only present when O_CREAT is set; reading it otherwise walks off
// the end of the argument list.
mode_t mode = 0;
unsigned int value = 0;
const int creating = (oflag & O_CREAT) != 0;
if (creating) {
va_list ap;
va_start(ap, oflag);
mode = (mode_t)va_arg(ap, unsigned int);
value = va_arg(ap, unsigned int);
va_end(ap);
}
if (jihad_is_pmlog_name(name)) {
if (creating) g_pmlogSemInitial = value;
pthread_once(&g_pmlogSemOnce, jihad_pmlog_sem_init);
return &g_pmlogSem;
}
if (!real_sem_open) return SEM_FAILED;
if (creating) return real_sem_open(name, oflag, mode, value);
return real_sem_open(name, oflag);
}
// PmLogLib closes the semaphore on unload. glibc's sem_close looks the pointer up in its
// table of NAMED semaphores, would not find ours, and would return EINVAL — harmless today,
// but only by luck. Answer for our own pointer and forward everything else.
__attribute__((visibility("default")))
int sem_close(sem_t* sem)
{
static int (*real_sem_close)(sem_t*);
if (!real_sem_close)
real_sem_close = (int (*)(sem_t*))dlsym(RTLD_NEXT, "sem_close");
// Deliberately NOT sem_destroy: the plugin can be unloaded and reloaded within one
// process (the scan does exactly that), and a destroyed static would be reused.
if (sem == &g_pmlogSem) return 0;
if (!real_sem_close) return 0;
return real_sem_close(sem);
}
+4
View File
@@ -9,6 +9,10 @@ SOURCES += [
'plugin-container.cpp',
]
# JihadPmLogSem.c lives here next to its only caller but is compiled by ipc/app, which is
# where the plugin-container PROGRAM is linked; this directory only produces an archive that
# that link does not use. See ipc/app/moz.build.
include('/ipc/chromium/chromium-config.mozbuild')
if CONFIG['OS_ARCH'] == 'WINNT':
+117
View File
@@ -3,10 +3,16 @@
* 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 <stdlib.h> // getenv / unsetenv (JIHAD: see main)
#include "nsXPCOM.h"
#include "nsXULAppAPI.h"
#include "nsAutoPtr.h"
// JihadPmLogSem.c — C, and deliberately carries no header, so it drops unchanged into both
// this build and the daemon's. Declared here rather than in one because that is the entire
// interface: one call whose only job is to keep the archive member in the link.
extern "C" int jihad_pmlog_sem_installed(void);
#ifdef XP_WIN
#include <windows.h>
// we want a wmain entry point
@@ -19,6 +25,82 @@
#include <unistd.h>
#endif
// JIHAD DIAGNOSTIC (R7): the child process dies with SIGSEGV between a successful
// InitForChrome and handling its first IPC message, and the device's own minicore reporter
// is out of report slots ("no room for new reports"), so nothing records where. This prints
// the faulting address, a backtrace, and /proc/self/maps to stderr — which the child
// inherits from the daemon, so it lands in the variant's daemon.log. The maps dump is the
// part that matters on ARM: even a single frame is enough to name the faulting library.
#if defined(OS_POSIX) || defined(XP_UNIX)
#include <execinfo.h>
#include <signal.h>
#include <ucontext.h>
#include <stdio.h>
#include <string.h>
#include <unistd.h>
static void
jihad_child_fault_handler(int sig, siginfo_t* info, void* ctx)
{
void* frames[32];
int n = backtrace(frames, 32);
fprintf(stderr, "[jihad-npapi-child] FAULT sig=%d addr=%p pid=%d frames=%d\n",
sig, info ? info->si_addr : nullptr, (int)getpid(), n);
// backtrace() returns nothing useful from a signal frame on ARM (no unwind tables
// through the trampoline), so take the registers straight out of the signal context.
// pc and lr against the maps below give the faulting library and offset, and
// addr2line on the unstripped build turns that into file:line.
#if defined(__arm__)
if (ctx) {
ucontext_t* uc = (ucontext_t*)ctx;
fprintf(stderr, "[jihad-npapi-child] FAULT pc=%08lx lr=%08lx sp=%08lx r0=%08lx\n",
(unsigned long)uc->uc_mcontext.arm_pc,
(unsigned long)uc->uc_mcontext.arm_lr,
(unsigned long)uc->uc_mcontext.arm_sp,
(unsigned long)uc->uc_mcontext.arm_r0);
}
#else
(void)ctx;
#endif
fflush(stderr);
backtrace_symbols_fd(frames, n, STDERR_FILENO);
FILE* maps = fopen("/proc/self/maps", "r");
if (maps) {
char line[512];
fputs("[jihad-npapi-child] --- maps ---\n", stderr);
while (fgets(line, sizeof(line), maps)) {
// Only the executable mappings; the full map is hundreds of lines of noise.
if (strstr(line, "r-xp")) {
fputs("[jihad-npapi-child] ", stderr);
fputs(line, stderr);
}
}
fclose(maps);
}
fflush(stderr);
// Restore the default disposition and re-raise so the exit status still says "crashed".
signal(sig, SIG_DFL);
raise(sig);
}
static void
jihad_install_child_fault_handler()
{
struct sigaction sa;
memset(&sa, 0, sizeof(sa));
sa.sa_sigaction = jihad_child_fault_handler;
sa.sa_flags = SA_SIGINFO;
sigemptyset(&sa.sa_mask);
sigaction(SIGSEGV, &sa, nullptr);
sigaction(SIGBUS, &sa, nullptr);
sigaction(SIGILL, &sa, nullptr);
sigaction(SIGFPE, &sa, nullptr);
}
#endif
#ifdef MOZ_GMP
#include "GMPLoader.h"
@@ -38,6 +120,41 @@ content_process_main(int argc, char* argv[])
return 3;
}
#if defined(OS_POSIX) || defined(XP_UNIX)
jihad_install_child_fault_handler();
#endif
// JIHAD (R7): force JihadPmLogSem.o into the link and say so. It interposes sem_open for
// the single name "PmLogLib", without which dlopening the device's Flash deadlocks this
// process forever — libPmLogLib's constructor waits on a named semaphore created by
// webOS's glibc 2.8, whose sem_t value word our glibc 2.23 reads as "no tokens, waiters
// pending". The call is also the only cheap proof the interposer survived the link: it
// lives in a static archive, and an archive member with no undefined symbol to satisfy
// is silently dropped. See render/goanna/JihadPmLogSem.c.
fprintf(stderr, "[jihad-npapi-child] pmlog sem interposition=%d\n",
jihad_pmlog_sem_installed());
// JIHAD DIAGNOSTIC (R7): the child's own view of its argv. Doubles as the control that
// child stderr reaches the parent's log at all — every other child probe is worthless
// until this line is seen.
fprintf(stderr, "[jihad-npapi-child] main argc=%d\n", argc);
for (int i = 0; i < argc; i++) {
fprintf(stderr, "[jihad-npapi-child] argv[%d]=%s\n", i, argv[i]);
}
fflush(stderr);
// JIHAD (2026-10-05): DO NOT HAND OUR LIBRARY PATH TO WHAT THE PLUGIN RUNS.
// This process is started through the bundled loader with an explicit --library-path,
// which is what its own later dlopen()s search; the LD_LIBRARY_PATH it also inherits
// is not needed by it. But it is inherited again by anything the plugin spawns, and
// Flash for Linux runs `sh -c "ps x | grep netscape"` at start-up: the device's own
// /bin/sh (built for its glibc 2.8) then loads the bundled glibc 2.23 and dies with
// SIGSEGV, leaving a crash report in /var/log/reports for every page with Flash on it.
// Only when the bundled loader is in use; a desktop build keeps its environment.
if (getenv("MOZ_CHILD_PROCESS_LOADER")) {
unsetenv("LD_LIBRARY_PATH");
}
#ifdef MOZ_GMP
XREChildData childData;
#endif
+28
View File
@@ -160,6 +160,13 @@ GeckoChildProcessHost::GetPathToBinary(FilePath& exePath, GeckoProcessType proce
exePath = exePath.AppendASCII(MOZ_CHILD_PROCESS_NAME);
// JIHAD DIAGNOSTIC (R7): the resolved child path, and which branch produced it. In this
// embedding the daemon is exec'd as `./ld-2.23.so … ./jihad-browserserver`, so argv[0] is the
// LOADER — if the directory-service branch is not taken, the fallback dirname(argv[0]) is
// relative to that, not to the daemon.
fprintf(stderr, "[jihad-npapi] child binary path=%s (dirService=%d)\n",
exePath.value().c_str(), (int)ShouldHaveDirectoryService());
return BinaryPathType::PluginContainer;
}
@@ -662,6 +669,27 @@ GeckoChildProcessHost::PerformAsyncLaunchInternal(std::vector<std::string>& aExt
std::vector<std::string> childArgv;
// Optional ELF loader prefix for the child process.
//
// An embedder whose bundled glibc differs from the host system's cannot exec the child
// binary directly: its PT_INTERP names the SYSTEM loader, which then resolves the child's
// libraries against the bundled set and dies before main(). Exporting these two variables
// makes the child be launched exactly the way such an embedder launches itself —
// <loader> --library-path <path> <child> <args…>
// — which runs the child in the same process image (no extra fork, no shell hop, so the
// pid we hand to waitpid and the inherited IPC channel fd are unaffected).
//
// Both unset is upstream behaviour, unchanged.
const char* childLoader = PR_GetEnv("MOZ_CHILD_PROCESS_LOADER");
if (childLoader && *childLoader) {
childArgv.push_back(childLoader);
const char* loaderLibPath = PR_GetEnv("MOZ_CHILD_PROCESS_LOADER_PATH");
if (loaderLibPath && *loaderLibPath) {
childArgv.push_back("--library-path");
childArgv.push_back(loaderLibPath);
}
}
childArgv.push_back(exePath.value());
if (pathType == BinaryPathType::Self) {
+8
View File
@@ -8,6 +8,14 @@
#include "jit/x86-shared/BaseAssembler-x86-shared.h"
// Jihad Browser: GCC 9+ -Wformat-overflow raises false positives on the debug
// spew() format strings below (it deduces a null %s arg via value-range
// analysis). js/src forces -Werror=format, which re-escalates this even when
// downgraded on the command line, so silence the diagnostic at the source.
#if defined(__GNUC__) && !defined(__clang__)
# pragma GCC diagnostic ignored "-Wformat-overflow"
#endif
namespace js {
namespace jit {
+19
View File
@@ -673,6 +673,13 @@ nsXULPopupManager::ShowMenu(nsIContent *aMenu,
bool aSelectFirstItem,
bool aAsynchronous)
{
// JIHAD popup instrumentation: the toolbarbutton type="menu" path (about:addons
// utils menu) enters here; if this line never fires, the DOM activation upstream
// (XUL button command/mousedown handling) is where the tap died.
if (getenv("JIHAD_OFFSCREEN")) {
fprintf(stderr, "[jihad-popup] ShowMenu menu=%p async=%d\n",
(void*)aMenu, (int)aAsynchronous);
}
// generate any template content first. Otherwise, the menupopup may not
// have been created yet.
if (aMenu) {
@@ -897,6 +904,18 @@ nsXULPopupManager::ShowPopupCallback(nsIContent* aPopup,
nsPopupType popupType = aPopupFrame->PopupType();
bool ismenu = (popupType == ePopupTypeMenu);
// JIHAD popup instrumentation (2026-08-02): EVERY popup that actually opens funnels
// through here. Silent until now — "menu never opened" vs "opened but painted
// nowhere" were indistinguishable in the headless daemon.
if (getenv("JIHAD_OFFSCREEN")) {
nsRect r = aPopupFrame->GetRect();
fprintf(stderr, "[jihad-popup] ShowPopupCallback type=%d ctx=%d frame=%p rect=%d,%d %dx%d hasView=%d hasWidget=%d\n",
(int)popupType, (int)aIsContextMenu, (void*)aPopupFrame,
r.x, r.y, r.width, r.height,
(int)aPopupFrame->HasView(),
(int)(aPopupFrame->GetView() && aPopupFrame->GetView()->HasWidget()));
}
nsMenuChainItem* item =
new nsMenuChainItem(aPopupFrame, aIsContextMenu, popupType);
if (!item)
+63 -2
View File
@@ -40,6 +40,9 @@
X(pa_operation_get_state) \
X(pa_operation_unref) \
X(pa_proplist_gets) \
X(pa_proplist_new) \
X(pa_proplist_sets) \
X(pa_proplist_free) \
X(pa_rtclock_now) \
X(pa_stream_begin_write) \
X(pa_stream_cancel_write) \
@@ -53,6 +56,7 @@
X(pa_stream_get_state) \
X(pa_stream_get_time) \
X(pa_stream_new) \
X(pa_stream_new_with_proplist) \
X(pa_stream_set_state_callback) \
X(pa_stream_set_write_callback) \
X(pa_stream_unref) \
@@ -114,8 +118,20 @@ struct cubeb_stream {
int shutdown;
float volume;
cubeb_state state;
int jihad_active; /* Jihad: counted in jihad_pulse_active_count while this stream is playing */
};
/* Jihad/webOS: number of currently-PLAYING (uncorked) output streams. The daemon
(render/goanna/BrowserPageGoanna) polls jihad_pulse_active_streams() and, on 0<->N transitions,
runs $JIHAD_STATE_DIR/audio-{on,off}.sh to hold the audiod media scenario + force the speaker
mixer, so engine media audio respects the system volume and can reach the built-in speaker (the
media.role set below gets the stream onto the media policy path in the first place). Weak-linked
from the daemon; on desktop nothing reads it and it is inert. */
static int jihad_pulse_active_count = 0;
/* visibility("default"): libxul is built -fvisibility=hidden, so export it explicitly (same as the
MOZ_EXPORT on jihad_plugin_frame_seq) or the daemon's weak extern resolves to null. */
__attribute__((visibility("default"))) int jihad_pulse_active_streams(void) { return jihad_pulse_active_count; }
static const float PULSE_NO_GAIN = -1.0;
enum cork_state {
@@ -680,7 +696,41 @@ create_pa_stream(cubeb_stream * stm,
ss.rate = stream_params->rate;
ss.channels = stream_params->channels;
*pa_stm = WRAP(pa_stream_new)(stm->context->context, stream_name, &ss, NULL);
/* Jihad/webOS: module-palm-policy powers the hardware codec route (and the built-in speaker)
only for a stream it CLASSIFIES as media, via a pa_proplist property our stream otherwise
lacks (see impl-audio-backend.md). The exact property/value is not yet pinned, so read
"key=value" lines from a WRITABLE file, $JIHAD_STATE_DIR/pulse.props, and set each on the
stream proplist — this lets the routing property be tuned on-device without a libxul rebuild.
With no file present, default to media.role=music. On desktop JIHAD_STATE_DIR is unset, no
file is read, and the default is a harmless standard PulseAudio role. */
pa_proplist * pl = WRAP(pa_proplist_new)();
int have_props = 0;
char const * sd = getenv("JIHAD_STATE_DIR");
if (sd && *sd) {
char path[512];
snprintf(path, sizeof(path), "%s/pulse.props", sd);
FILE * pf = fopen(path, "r");
if (pf) {
char line[256];
while (fgets(line, sizeof(line), pf)) {
char * nl = strchr(line, '\n'); if (nl) { *nl = '\0'; }
if (line[0] == '\0' || line[0] == '#') { continue; }
char * eq = strchr(line, '=');
if (eq) { *eq = '\0'; WRAP(pa_proplist_sets)(pl, line, eq + 1); have_props = 1; }
}
fclose(pf);
}
}
if (!have_props) {
/* Default: classify as media so the stream takes the webOS media-policy path (the pmedia
virtual sink) instead of sitting anonymously on pcm_output at a fixed 100%. That classification
is what lets the daemon's audio-on.sh hold the audiod media scenario (system-volume-respecting)
and force the speaker route on top. See impl-audio-backend.md. Override/disable via the props
file. */
WRAP(pa_proplist_sets)(pl, "media.role", "music");
}
*pa_stm = WRAP(pa_stream_new_with_proplist)(stm->context->context, stream_name, &ss, NULL, pl);
WRAP(pa_proplist_free)(pl);
return (*pa_stm == NULL) ? CUBEB_ERROR : CUBEB_OK;
}
@@ -752,8 +802,16 @@ pulse_stream_init(cubeb * context,
WRAP(pa_stream_set_write_callback)(stm->output_stream, stream_write_callback, stm);
battr = set_buffering_attribute(latency_frames, &stm->output_sample_spec);
/* Jihad/webOS: module-palm-policy powers the hardware codec route only for a stream created on
a named virtual sink (pmedia); a NULL/default sink lands on pcm_output and is muted to 0%.
When no explicit device was requested, honour JIHAD_PULSE_SINK so the device build can
target "pmedia". No effect on desktop (env unset -> NULL -> default sink, upstream behaviour). */
char const * jihad_out_sink = (char const *) output_device;
if (!jihad_out_sink) {
jihad_out_sink = getenv("JIHAD_PULSE_SINK");
}
WRAP(pa_stream_connect_playback)(stm->output_stream,
output_device,
jihad_out_sink,
&battr,
PA_STREAM_AUTO_TIMING_UPDATE | PA_STREAM_INTERPOLATE_TIMING |
PA_STREAM_START_CORKED | PA_STREAM_ADJUST_LATENCY,
@@ -821,6 +879,7 @@ static void
pulse_stream_destroy(cubeb_stream * stm)
{
stream_cork(stm, CORK);
if (stm->jihad_active) { stm->jihad_active = 0; if (jihad_pulse_active_count > 0) jihad_pulse_active_count--; }
WRAP(pa_threaded_mainloop_lock)(stm->context->mainloop);
if (stm->output_stream) {
@@ -864,6 +923,7 @@ pulse_stream_start(cubeb_stream * stm)
{
stm->shutdown = 0;
stream_cork(stm, UNCORK | NOTIFY);
if (stm->output_stream && !stm->jihad_active) { stm->jihad_active = 1; jihad_pulse_active_count++; }
if (stm->output_stream && !stm->input_stream) {
/* On output only case need to manually call user cb once in order to make
@@ -890,6 +950,7 @@ pulse_stream_stop(cubeb_stream * stm)
WRAP(pa_threaded_mainloop_unlock)(stm->context->mainloop);
stream_cork(stm, CORK | NOTIFY);
if (stm->jihad_active) { stm->jihad_active = 0; if (jihad_pulse_active_count > 0) jihad_pulse_active_count--; }
return CUBEB_OK;
}
+4 -4
View File
@@ -245,7 +245,7 @@ pref("dom.compartment_per_addon", true);
pref("browser.sessionhistory.max_total_viewers", -1);
// Whether to store 'about:newtab' in the session history, disabled by default.
// See https://github.com/MoonchildProductions/UXP/issues/719
// See (upstream issue 719)
pref("browser.newtabpage.add_to_session_history", false);
// Determines whether the browser's current theme should be light or dark.
@@ -1193,7 +1193,7 @@ pref("dom.webapps.useCurrentProfile", false);
// Use incremental cycle collection?
// In practice this gave a noticeable performance hit. Default off.
// See forum topic https://forum.palemoon.org/viewtopic.php?f=62&t=29887
// See forum topic (upstream forum)
pref("dom.cycle_collector.incremental", false);
// Parsing perf prefs. For now just mimic what the old code did.
@@ -2232,7 +2232,7 @@ pref("extensions.blocklist.interval", 86400);
// Required blocklist freshness for OneCRL OCSP bypass
// (default is 1.25x extensions.blocklist.interval, or 30 hours)
pref("security.onecrl.maximum_staleness_in_seconds", 108000);
pref("extensions.blocklist.url", "https://blocklist.basilisk-browser.org/blocklist/%APP_ID%/%APP_VERSION%/%PRODUCT%/%BUILD_ID%/%BUILD_TARGET%/%LOCALE%/%CHANNEL%/%OS_VERSION%/");
pref("extensions.blocklist.url", "about:blank");
pref("extensions.blocklist.detailsURL", "https://www.mozilla.com/%LOCALE%/blocklist/");
pref("extensions.blocklist.itemURL", "https://blocklist.addons.mozilla.org/%LOCALE%/%APP%/blocked/%blockID%");
// Controls what level the blocklist switches from warning about items to forcibly
@@ -4779,7 +4779,7 @@ pref("network.captive-portal-service.maxInterval", 1500000); // 25 minutes
pref("network.captive-portal-service.backoffFactor", "5.0");
pref("network.captive-portal-service.enabled", false);
pref("captivedetect.canonicalURL", "http://detectportal.palemoon.org/success.txt");
pref("captivedetect.canonicalURL", "http://example.com/success.txt");
pref("captivedetect.canonicalContent", "success\n");
pref("captivedetect.maxWaitingTime", 5000);
pref("captivedetect.pollingTime", 3000);
+35 -3
View File
@@ -2146,12 +2146,25 @@ MOZ_ARG_DISABLE_BOOL(npapi,
MOZ_ENABLE_NPAPI=,
MOZ_ENABLE_NPAPI=1)
MOZ_ENABLE_NPAPI_GTK2=1
MOZ_ARG_DISABLE_BOOL(npapi-gtk2,
[ --disable-npapi-gtk2 Disable GTK2 support for NPAPI plugins ],
MOZ_ENABLE_NPAPI_GTK2=,
MOZ_ENABLE_NPAPI_GTK2=1)
if test "$MOZ_ENABLE_NPAPI"
then
AC_DEFINE(MOZ_ENABLE_NPAPI)
fi
if test "$MOZ_ENABLE_NPAPI" -a "$MOZ_ENABLE_NPAPI_GTK2"
then
AC_DEFINE(MOZ_ENABLE_NPAPI_GTK2)
fi
AC_SUBST(MOZ_ENABLE_NPAPI)
AC_SUBST(MOZ_ENABLE_NPAPI_GTK2)
dnl ========================================================
dnl =
@@ -2174,6 +2187,12 @@ gtk2)
USE_FC_FREETYPE=1
;;
headless)
dnl Jihad: X/GTK-free headless toolkit. Fonts via fontconfig/freetype
dnl (no X); the widget backend is widget/headless (PuppetWidget-based).
USE_FC_FREETYPE=1
;;
cocoa)
LDFLAGS="$LDFLAGS -framework Cocoa -lobjc"
# Use -Wl as a trick to avoid -framework and framework names from
@@ -2200,6 +2219,10 @@ gtk*)
;;
esac
if test "$MOZ_WIDGET_TOOLKIT" = gtk2 -a -z "$MOZ_ENABLE_NPAPI_GTK2"; then
AC_MSG_ERROR([--disable-npapi-gtk2 cannot be used with GTK2 browser builds.])
fi
if test "$COMPILE_ENVIRONMENT"; then
if test "$MOZ_WIDGET_TOOLKIT" = gtk3; then
PKG_CHECK_MODULES(MOZ_GTK3, gtk+-3.0 >= $GTK3_VERSION gtk+-unix-print-3.0 glib-2.0 gobject-2.0 $GDK_PACKAGES)
@@ -2210,8 +2233,9 @@ if test "$COMPILE_ENVIRONMENT"; then
dnl are suppressed by widget/gtk/compat-gtk3/gdk/gdkversionmacros.h.
AC_DEFINE_UNQUOTED(GDK_VERSION_MAX_ALLOWED,$GDK_VERSION_MAX_ALLOWED)
GLIB_VERSION_MAX_ALLOWED=GLIB_VERSION_2_32
dnl We want to check GTK2 packages if NPAPI is enabled, even if building with GTK3.
if test "$MOZ_ENABLE_NPAPI"; then
dnl GTK2 NPAPI plugin support needs GTK2 packages, even when the browser
dnl itself is built against GTK3.
if test "$MOZ_ENABLE_NPAPI" -a "$MOZ_ENABLE_NPAPI_GTK2"; then
MOZ_CHECK_GTK2=1
fi
fi
@@ -2229,7 +2253,7 @@ if test "$COMPILE_ENVIRONMENT"; then
if test "$MOZ_CHECK_GTK2"; then
PKG_CHECK_MODULES(MOZ_GTK2, gtk+-2.0 >= $GTK2_VERSION gtk+-unix-print-2.0 glib-2.0 >= $GLIB_VERSION gobject-2.0 $GDK_PACKAGES, ,
[AC_MSG_ERROR([ * * * Could not find gtk+-2.0. Required for NPAPI plugins. To build without NPAPI, add --disable-npapi to your .mozconfig and clobber.])])
[AC_MSG_ERROR([ * * * Could not find gtk+-2.0. Required for GTK2 browser builds and GTK2 NPAPI plugin support. To build a GTK3 browser with NPAPI but without GTK2 NPAPI plugin support, add --disable-npapi-gtk2 to your .mozconfig and clobber.])])
MOZ_GTK2_CFLAGS="-I${_topsrcdir}/widget/gtk/compat $MOZ_GTK2_CFLAGS"
fi
fi
@@ -4415,6 +4439,14 @@ if test "$MOZ_TREE_CAIRO"; then
MOZ_ENABLE_CAIRO_FT=1
CAIRO_FT_CFLAGS="$FT2_CFLAGS"
AC_SUBST_LIST(CAIRO_FT_CFLAGS)
dnl Jihad headless: no toolkit pkg-config supplies the freetype include
dnl path (gtk normally carries it in TK_CFLAGS via its gtk cflags).
dnl Inject it so gfx/thebes and other cairo-ft consumers find
dnl <ft2build.h> and the fontconfig headers.
if test "$MOZ_WIDGET_TOOLKIT" = "headless"; then
TK_CFLAGS="$TK_CFLAGS $FT2_CFLAGS"
TK_LIBS="$TK_LIBS $FT2_LIBS"
fi
fi
case "$MOZ_WIDGET_TOOLKIT" in
+34
View File
@@ -150,6 +150,35 @@ bool EnsureNSSInitialized(EnsureNSSOperator op)
static bool loading = false;
static int32_t haveLoaded = 0;
// Jihad: in the embedding render daemon there is no early main-thread PSM init, so
// necko's SOCKET thread reaches here first on the first https load and the
// do_GetService(PSM) below would construct nsNSSComponent off the main thread — its
// ctor MOZ_RELEASE_ASSERT(NS_IsMainThread()) then aborts the process ("loads forever").
// Forward to the main thread synchronously (same as the content-process path above) so
// NSS constructs on the main thread; the socket thread then reuses the singleton.
//
// FAST PATH (critical): once NSS is loaded, an "ensure" op just needs to confirm that,
// which is a lock-free atomic read valid on ANY thread — so return immediately WITHOUT
// marshaling. Marshaling every ensure call floods the main thread with SyncRunnables on
// an https-heavy page (one per SSL socket op) and the load hangs/loops. Only the FIRST
// init (haveLoaded still 0) needs the main-thread round-trip.
if ((op == nssEnsure || op == nssEnsureOnChromeOnly || op == nssEnsureChromeOrContent) &&
PR_AtomicAdd(&haveLoaded, 0)) {
return true;
}
if (!NS_IsMainThread()) {
nsCOMPtr<nsIThread> mainThread;
if (NS_FAILED(NS_GetMainThread(getter_AddRefs(mainThread)))) {
return false;
}
bool result = false;
mozilla::SyncRunnable::DispatchToThread(mainThread,
new mozilla::SyncRunnable(NS_NewRunnableFunction([op, &result]() {
result = EnsureNSSInitialized(op);
})));
return result;
}
switch (op)
{
// In following 4 cases we are protected by monitor of XPCOM component
@@ -1793,12 +1822,17 @@ nsNSSComponent::InitializeNSS()
// pref has been set to "true", attempt to initialize with no DB.
if (nocertdb || init_rv != SECSuccess) {
init_rv = NSS_NoDB_Init(nullptr);
PRErrorCode prerr = PR_GetError();
fprintf(stderr, "[jihad-bs] NSS_NoDB_Init rv=%d PRerr=%d (%s) osErr=%d\n",
(int)init_rv, (int)prerr, PR_ErrorToName(prerr), (int)PR_GetOSError());
}
if (init_rv != SECSuccess) {
fprintf(stderr, "[jihad-bs] InitializeNSS FAILED (panicking)\n");
MOZ_LOG(gPIPNSSLog, LogLevel::Error, ("could not initialize NSS - panicking\n"));
return NS_ERROR_NOT_AVAILABLE;
}
fprintf(stderr, "[jihad-bs] InitializeNSS OK\n");
// ensure we have an initial value for the content signer root
mContentSigningRootHash =
+81 -5
View File
@@ -293,6 +293,8 @@
firstFrameShown : false,
timeUpdateCount : 0,
maxCurrentTimeSeen : 0,
_lastDurationShown : NaN,
_scrubTimer : null,
_isAudioOnly : false,
get isAudioOnly() { return this._isAudioOnly; },
set isAudioOnly(val) {
@@ -335,7 +337,14 @@
// Explicitly hide the status fader if this
// is audio only until bug 619421 is fixed.
if (this.isAudioOnly)
// Jihad: upstream suppressed the overlay for audio-only content (no video box
// to draw a throbber over), but it also hid the ERROR state — so an <audio>
// whose format cannot be decoded (MP3/AAC have no decoder in this build; the
// element ends err=4 MEDIA_ERR_SRC_NOT_SUPPORTED) rendered a live-looking
// control bar with no failure shown: the forbidden "works-looking but silent"
// shape (cavekit-gre-widgets.md R1). Keep the throbber/loading suppression, but
// let a real, non-suppressed media error still surface for audio-only too.
if (this.isAudioOnly && !(this.video.error && !this.suppressError))
show = false;
this.log("Status overlay: seeking=" + this.video.seeking +
@@ -486,6 +495,7 @@
case "play":
this.setPlayButtonState(false);
this.setupStatusFader();
this.startScrubTimer(); // Jihad: smooth the scrubber (see startScrubTimer)
if (!this._triggeredByControls && this.dynamicControls && this.videocontrols.isTouchControl)
this.startFadeOut(this.controlBar);
if (!this._triggeredByControls)
@@ -497,14 +507,21 @@
// while dragging the scrubber, don't change the button state.
if (!this.scrubber.isDragging)
this.setPlayButtonState(true);
this.stopScrubTimer(); // Jihad: no interpolation while paused
this.setupStatusFader();
break;
case "ended":
this.setPlayButtonState(true);
// We throttle timechange events, so the thumb might not be
// exactly at the end when the video finishes.
this.showPosition(Math.round(this.video.currentTime * 1000),
Math.round(this.video.duration * 1000));
this.stopScrubTimer(); // Jihad: playback done, stop interpolating
// timeupdate is throttled, so the last position reported before "ended"
// is a few ms short of the duration, which left the thumb resting short of
// the end on the offscreen surface ("doesn't come to rest at clip end").
// At "ended" the position IS the end, so snap the thumb there. Streams
// (non-finite duration) keep the last-seen position.
var endDurMs = Math.round(this.video.duration * 1000);
var endPosMs = isFinite(endDurMs) ? endDurMs
: Math.round(this.video.currentTime * 1000);
this.showPosition(endPosMs, endDurMs);
this.startFadeIn(this.controlBar);
this.setupStatusFader();
break;
@@ -607,6 +624,10 @@
case "playing":
case "canplay":
case "canplaythrough":
// Jihad: resume smoothing if playback picked back up after buffering
// (idempotent; no-op if already running or if still paused/ended).
if (!this.video.paused && !this.video.ended)
this.startScrubTimer();
this.setupStatusFader();
break;
case "error":
@@ -641,6 +662,7 @@
},
terminateEventListeners : function () {
this.stopScrubTimer(); // Jihad: don't leak the smoothing interval on detach
if (this.videoEvents) {
for (let event of this.videoEvents) {
this.video.removeEventListener(event, this, {
@@ -746,6 +768,19 @@
showDuration : function (duration) {
let isInfinite = (duration == Infinity);
// Jihad: showPosition() calls this every timeupdate, but for a normal clip the
// duration is finite and constant. Re-running it each tick re-sets scrubber.max
// (forcing the native <scale> to re-lay-out its whole range -> a jittery thumb on
// the offscreen surface) and re-sets scrubberThumb.showHours via setAttribute
// (restyling the thumb bubble every tick -> the "0:00 flashes in and out" defect).
// Skip the redundant work when the finite duration is unchanged. Streams (Infinite
// /NaN duration) fall through because scrubber.max must track the growing
// maxCurrentTimeSeen.
if (!isInfinite && !isNaN(duration) && duration === this._lastDurationShown)
return;
this._lastDurationShown = duration;
this.log("Duration is " + duration + "ms.\n");
if (isNaN(duration) || isInfinite)
@@ -802,6 +837,47 @@
this.scrubber.value = currentTime;
},
// Jihad: timeupdate fires only a few times a second, so the scrubber thumb steps
// forward in visible jumps ("moving in ticks"). currentTime is a live clock that
// advances continuously between those events, so while playing we drive the thumb
// from it on a short interval for smooth motion. Safe now that the offscreen ghost
// bug is fixed (per-buffer damage, BrowserPageGoanna): the daemon coalesces the
// extra damage and paces the actual repaints, so this cannot over-drive it. Stopped
// on pause/ended and when the binding detaches.
// Jihad: timeupdate fires only a few times a second, so the scrubber thumb steps
// forward in visible jumps ("moving in ticks"). currentTime is a live clock that
// advances continuously between those events, so while playing we drive the thumb
// from it on a short interval. The tick ONLY nudges the thumb position
// (scrubber.value) — never showDuration/showPosition, which re-set scrubber.max and
// thrash the scale range — and is guarded on a known finite duration so it never
// runs before metadata (max would be 0 and the thumb would freeze). Bare setInterval
// matches the touch binding's timer usage.
startScrubTimer : function() {
if (this._scrubTimer)
return;
var self = this;
this._scrubTimer = setInterval(function() {
if (self.video.paused || self.video.ended || self.scrubber.isDragging)
return;
var dur = self.video.duration;
if (!(dur > 0) || !isFinite(dur))
return;
var ct = Math.round(self.video.currentTime * 1000);
if (ct > self.maxCurrentTimeSeen)
self.maxCurrentTimeSeen = ct;
self.positionLabel.setAttribute("value", self.formatTime(ct));
self.scrubber.value = ct;
}, 40); // 25fps: the practical ceiling. Faster (tried 25ms/40fps) overruns the
// adapter's drain rate for this region — paints defer and the thumb +
// time freeze. The offscreen paint pipeline caps the achievable smoothness.
},
stopScrubTimer : function() {
if (this._scrubTimer) {
clearInterval(this._scrubTimer);
this._scrubTimer = null;
}
},
showBuffered : function() {
function bsearch(haystack, needle, cmp) {
var length = haystack.length;
+4
View File
@@ -212,6 +212,10 @@ OS_LIBS += CONFIG['ICONV_LIBS']
if CONFIG['MOZ_WIDGET_TOOLKIT'] in ('cocoa', 'uikit'):
OS_LIBS += CONFIG['TK_LIBS']
if CONFIG['MOZ_WIDGET_TOOLKIT'] == 'headless':
# Jihad headless: TK_LIBS carries freetype/fontconfig (no gtk/X libs).
OS_LIBS += CONFIG['TK_LIBS']
if CONFIG['MOZ_SNDIO']:
OS_LIBS += [
'sndio',
+6 -3
View File
@@ -48,7 +48,7 @@ set_config('L10NBASEDIR', l10n_base)
# reason.
option('--enable-default-toolkit', nargs=1,
choices=('cairo-windows', 'cairo-gtk2', 'cairo-gtk2-x11', 'cairo-gtk3',
'cairo-cocoa', 'cairo-uikit'),
'cairo-cocoa', 'cairo-uikit', 'cairo-headless'),
help='Select default toolkit')
@depends('--enable-default-toolkit', target, '--help')
@@ -63,7 +63,8 @@ def toolkit(value, target, _):
elif target.os == 'iOS':
platform_choices = ('cairo-uikit',)
else:
platform_choices = ('cairo-gtk3', 'cairo-gtk2', 'cairo-gtk2-x11')
platform_choices = ('cairo-gtk3', 'cairo-gtk2', 'cairo-gtk2-x11',
'cairo-headless')
if value:
if value[0] not in platform_choices:
@@ -160,7 +161,9 @@ set_define(gl_provider_define, True)
# ==============================================================
@depends(toolkit)
def pdf_printing(toolkit):
if toolkit in ('windows', 'gtk2', 'gtk3'):
# 'headless' included: the cairo PDF surface is X/GTK-free and the headless
# gfx build uses PrintTargetPDF, so CAIRO_HAS_PDF_SURFACE must be defined.
if toolkit in ('windows', 'gtk2', 'gtk3', 'headless'):
return True
@depends(pdf_printing)
+25 -8
View File
@@ -1878,13 +1878,21 @@ var AddonManagerInternal = {
// notify front-end that install events came from the outer-browser (the
// main tab's browser). Check this by seeing if the browser we've been
// passed is in a content type docshell and if so get the outer-browser.
// JIHAD (browser-services R3): aBrowser is documented above as optional ("aSource must be
// a nsIDOMElement, or null") and every caller in a browser passes the <browser> element the
// page lives in — but this embedding has no browser chrome and therefore no such element, so
// the walk below dereferenced null and cancelled the install before the prompt was reached.
// With no element there is no outer-browser to notify either, so the whole remapping is
// simply skipped and topBrowser stays null, which the listener path already tolerates.
let topBrowser = aBrowser;
let docShell = aBrowser.ownerDocument.defaultView
.QueryInterface(Ci.nsIInterfaceRequestor)
.getInterface(Ci.nsIDocShell)
.QueryInterface(Ci.nsIDocShellTreeItem);
if (docShell.itemType == Ci.nsIDocShellTreeItem.typeContent)
topBrowser = docShell.chromeEventHandler;
if (aBrowser) {
let docShell = aBrowser.ownerDocument.defaultView
.QueryInterface(Ci.nsIInterfaceRequestor)
.getInterface(Ci.nsIDocShell)
.QueryInterface(Ci.nsIDocShellTreeItem);
if (docShell.itemType == Ci.nsIDocShellTreeItem.typeContent)
topBrowser = docShell.chromeEventHandler;
}
try {
let weblistener = Cc["@mozilla.org/addons/web-install-listener;1"].
@@ -1898,7 +1906,13 @@ var AddonManagerInternal = {
aInstalls, aInstalls.length);
return;
}
else if (!aBrowser.contentPrincipal || !aInstallingPrincipal.subsumes(aBrowser.contentPrincipal)) {
// JIHAD: the same optional-aBrowser problem as above. This check compares the principal
// that triggered the install against the principal of the page currently in the browser
// element, to catch a page that navigated away mid-install; with no browser element there
// is nothing to compare against and no navigation to race, so the check is skipped rather
// than treated as a failed comparison (which cancelled every install).
else if (aBrowser &&
(!aBrowser.contentPrincipal || !aInstallingPrincipal.subsumes(aBrowser.contentPrincipal))) {
for (let install of aInstalls)
install.cancel();
@@ -1912,7 +1926,10 @@ var AddonManagerInternal = {
// The installs may start now depending on the web install listener,
// listen for the browser navigating to a new origin and cancel the
// installs in that case.
new BrowserListener(aBrowser, aInstallingPrincipal, aInstalls);
// Same reason: BrowserListener watches a browser element for a navigation that should
// cancel the installs. With no element there is nothing to attach to.
if (aBrowser)
new BrowserListener(aBrowser, aInstallingPrincipal, aInstalls);
if (!this.isInstallAllowed(aMimetype, aInstallingPrincipal)) {
if (weblistener.onWebInstallBlocked(topBrowser, aInstallingPrincipal.URI,
+16 -8
View File
@@ -60,7 +60,7 @@ amContentHandler.prototype = {
// When running in the main process this might be a frame inside an
// in-content UI page, walk up to find the first frame element in a chrome
// privileged document
let element = window.frameElement;
let element = window ? window.frameElement : null;
let ssm = Services.scriptSecurityManager;
while (element && !ssm.isSystemPrincipal(element.ownerDocument.nodePrincipal))
element = element.ownerDocument.defaultView.frameElement;
@@ -77,13 +77,21 @@ amContentHandler.prototype = {
}
}
// Fall back to sending through the message manager
let messageManager = window.QueryInterface(Ci.nsIInterfaceRequestor)
.getInterface(Ci.nsIDocShell)
.QueryInterface(Ci.nsIInterfaceRequestor)
.getInterface(Ci.nsIContentFrameMessageManager);
messageManager.sendAsyncMessage(MSG_INSTALL_ADDONS, installs);
// JIHAD: this embedding is a single top-level content docShell with no browser chrome
// above it — no chrome-privileged frameElement to find above, and no
// nsIContentFrameMessageManager below — so both upstream paths fail, and the failure was
// silent: the load had already been cancelled above, so following a plain link to an
// .xpi (or typing its address) did nothing at all, with no prompt and no error
// (device, 2026-10-01). InstallTrigger.install() was fixed the same way in
// amInstallTrigger.js; this is the other door into the same room. Call the integration
// service directly — the object the parent process reaches when it handles
// MSG_INSTALL_ADDONS; only the transport is skipped, because there is no second process.
let integration = Cc["@mozilla.org/addons/integration;1"]
.getService(Ci.nsIMessageListener).wrappedJSObject;
integration.installAddonsFromWebpage(installs.mimetype, null,
installs.triggeringPrincipal, installs.uris,
installs.hashes, installs.names, installs.icons,
null);
},
classID: Components.ID("{7beb3ba8-6ec3-41b4-b67c-da89b8518922}"),
+24 -8
View File
@@ -93,14 +93,30 @@ RemoteMediator.prototype = {
}
}
// Fall back to sending through the message manager
let messageManager = window.QueryInterface(Ci.nsIInterfaceRequestor)
.getInterface(Ci.nsIWebNavigation)
.QueryInterface(Ci.nsIDocShell)
.QueryInterface(Ci.nsIInterfaceRequestor)
.getInterface(Ci.nsIContentFrameMessageManager);
return messageManager.sendSyncMessage(MSG_INSTALL_ADDONS, installs)[0];
// JIHAD (browser-services R3): this embedding is a single top-level content docShell
// with NO browser chrome above it — there is no chrome-privileged frameElement to find
// above, and no nsIContentFrameMessageManager below, so BOTH upstream paths fail and
// InstallTrigger.install() threw NS_ERROR_UNEXPECTED at the getInterface below for every
// web install (measured 2026-08-03). Call the integration service directly: it is the same
// object, and the same method, that the parent process reaches when it handles the
// MSG_INSTALL_ADDONS message — only the transport is skipped, because there is no second
// process to send it to.
let integration = Cc["@mozilla.org/addons/integration;1"]
.getService(Ci.nsIMessageListener).wrappedJSObject;
let jihadCallback = null;
if (callbackID != -1) {
jihadCallback = {
onInstallEnded: (url, status) => {
try {
let cb = this._callbacks.get(callbackID);
if (cb) cb.callCallback(url, status);
} catch (e) {}
},
};
}
return integration.installAddonsFromWebpage(installs.mimetype, null, principal,
installs.uris, installs.hashes, installs.names,
installs.icons, jihadCallback);
},
_addCallback: function(callback, urls) {
@@ -6432,8 +6432,19 @@ AddonInternal.prototype = {
// Only do this for extensions. Other types should not be allowed.
if (this.type == "extension") {
for (let targetApp of this.targetApplications) {
if (targetApp.id == APPCOMPATID) // AppCompat GUID
if (targetApp.id == APPCOMPATID) { // AppCompat GUID
// An add-on may name BOTH the AppCompat GUID and the toolkit: the GUID entry
// written for the application that owns that GUID, at that application's
// versions, and the toolkit entry for "any other application on this platform".
// Taking the GUID entry regardless disabled exactly the add-ons that had gone
// to the trouble of declaring generic platform support (one whose GUID entry
// says 52.9.*, against an AppCompat version of 56.9). Where the GUID entry does
// not fit this application's AppCompat version and a toolkit entry was given,
// the toolkit entry is the one that describes us.
if (app && !this._fitsAppCompat(targetApp))
break;
return targetApp;
}
}
}
#endif
@@ -6441,6 +6452,21 @@ AddonInternal.prototype = {
return app;
},
#ifdef UXP_APPCOMPAT_GUID
// Whether aTargetApp (an AppCompat GUID entry) admits this application's AppCompat
// version, by the same rule isCompatibleWith applies: both bounds under strict
// compatibility, the lower bound alone otherwise.
_fitsAppCompat: function AddonInternal_fitsAppCompat(aTargetApp) {
let strict = !(this.type in COMPATIBLE_BY_DEFAULT_TYPES) ||
AddonManager.strictCompatibility || this.strictCompatibility ||
this.hasBinaryComponents;
if (Services.vc.compare(APPCOMPATVERSION, aTargetApp.minVersion) < 0)
return false;
return !strict || Services.vc.compare(APPCOMPATVERSION, aTargetApp.maxVersion) <= 0;
},
#endif
get blocklistState() {
let staticItem = findMatchingStaticBlocklistItem(this);
if (staticItem)
+2 -2
View File
@@ -3629,8 +3629,8 @@ XREMain::XRE_mainRun()
// reset the profile.
// For Basilisk and Pale Moon:
// Hard-code MOZ_APP_NAME to firefox because of hard-coded type in migrator.
aKey = (((MOZ_APP_NAME == "basilisk")
|| (MOZ_APP_NAME == "palemoon"))
aKey = (((MOZ_APP_NAME == "n/a")
|| (MOZ_APP_NAME == "n/a"))
? "firefox" : MOZ_APP_NAME);
}
+8
View File
@@ -76,6 +76,7 @@
#include "GMPLoader.h"
#endif
#include "mozilla/gfx/GPUProcessImpl.h"
#include "gfxPlatform.h"
#include "GeckoProfiler.h"
@@ -177,6 +178,13 @@ XRE_InitEmbedding2(nsIFile *aLibXULDirectory,
startupNotifier->Observe(nullptr, APPSTARTUP_TOPIC, nullptr);
// Jihad Browser: force full gfx init (gfxPlatform/gfxVars/gfxConfig) during
// embedding startup. XRE_InitEmbedding2 otherwise leaves gfx lazy, but the
// headless paint path (WindowSurfaceProvider -> gfxVars::UseXRender) touches
// gfxVars before anything triggers gfxPlatform::Init, crashing on a null
// singleton. XRE_main initializes gfx during normal startup.
gfxPlatform::GetPlatform();
return NS_OK;
}
+1
View File
@@ -4,6 +4,7 @@
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
#include "nsAppRunner.h"
#include "GeckoProfiler.h" // Jihad headless: PROFILER_LABEL_FUNC/js::ProfileEntry (was transitively included in the gtk unified build)
#include "nsToolkitCompsCID.h"
#include "nsXREDirProvider.h"
+3 -1
View File
@@ -73,7 +73,9 @@ else:
if CONFIG['GNU_CXX']:
CXXFLAGS += ['-Wno-error=shadow']
if 'gtk' in CONFIG['MOZ_WIDGET_TOOLKIT']:
if 'gtk' in CONFIG['MOZ_WIDGET_TOOLKIT'] or CONFIG['MOZ_WIDGET_TOOLKIT'] == 'headless':
# Jihad headless is a unix helper-app target: GNOME/GIO lookups are
# runtime do_GetService calls that degrade gracefully with no gnome libs.
UNIFIED_SOURCES += [
'unix/nsGNOMERegistry.cpp',
'unix/nsMIMEInfoUnix.cpp',
+717 -6
View File
@@ -21,6 +21,23 @@
#include "nsIWidgetListener.h"
#include "imgIContainer.h"
#include "nsView.h"
// Jihad offscreen render-document support:
#include "gfxContext.h"
#include "nsIDocShell.h"
#include "nsIPresShell.h"
#include "nsPresContext.h"
#include "nsServiceManagerUtils.h" // do_GetService (jihad_init_nss)
#include "nsThreadUtils.h" // NS_IsMainThread
// Jihad popup overlay composite (a popup is a separate display root — see
// jihad_offscreen_composite_popups at the bottom of this file):
#include "nsXULPopupManager.h"
#include "nsLayoutUtils.h"
#include "nsRenderingContext.h"
#include "nsMenuPopupFrame.h"
#include "nsMenuFrame.h"
#include "nsGkAtoms.h"
#include "nsIFrame.h"
#include "nsDisplayList.h" // nsDisplayListBuilderMode
using namespace mozilla;
using namespace mozilla::dom;
@@ -114,6 +131,18 @@ PuppetWidget::InfallibleCreate(nsIWidget* aParent,
mEnabled = true;
mVisible = true;
// JIHAD popup instrumentation (menupopup investigation 2026-08-02): today popup
// widget creation logs NOTHING, so "no popup created" and "popup created but
// painted nowhere" are indistinguishable — exactly the ambiguity that burned the
// holdAt and icon investigations. One line per widget creation, offscreen only.
if (getenv("JIHAD_OFFSCREEN")) {
fprintf(stderr, "[jihad-widget] create type=%d popup=%d hint=%d parent=%p bounds=%d,%d %dx%d\n",
aInitData ? (int)aInitData->mWindowType : -1,
(int)IsPopup(aInitData),
aInitData ? (int)aInitData->mPopupHint : -1,
(void*)aParent, aRect.x, aRect.y, aRect.width, aRect.height);
}
mDrawTarget = gfxPlatform::GetPlatform()->
CreateOffscreenContentDrawTarget(IntSize(1, 1), SurfaceFormat::B8G8R8A8);
@@ -201,6 +230,13 @@ PuppetWidget::Show(bool aState)
bool wasVisible = mVisible;
mVisible = aState;
// JIHAD popup instrumentation: a popup that opens/closes flips its widget's
// visibility — the cheapest reliable "did the menu actually open" signal.
if (mWindowType == eWindowType_popup && getenv("JIHAD_OFFSCREEN")) {
fprintf(stderr, "[jihad-widget] popup show=%d widget=%p bounds=%d,%d %dx%d\n",
(int)aState, (void*)this, mBounds.x, mBounds.y, mBounds.width, mBounds.height);
}
if (mChild) {
mChild->mVisible = aState;
}
@@ -231,6 +267,10 @@ PuppetWidget::Resize(double aWidth,
mBounds.SizeTo(LayoutDeviceIntSize(NSToIntRound(aWidth),
NSToIntRound(aHeight)));
if (JihadOffscreen()) {
JihadEnsureDrawTarget();
}
if (mChild) {
return mChild->Resize(aWidth, aHeight, aRepaint);
}
@@ -300,6 +340,11 @@ PuppetWidget::Invalidate(const LayoutDeviceIntRect& aRect)
return mChild->Invalidate(aRect);
}
// Jihad: record that content changed so the offscreen render daemon repaints
// (jihad_offscreen_take_dirty). Sticky — only the daemon's drain clears it.
mJihadDirty = true;
mJihadDirtyRect = mJihadDirtyRect.IsEmpty() ? aRect : mJihadDirtyRect.Union(aRect);
mDirtyRegion.Or(mDirtyRegion, aRect);
if (!mDirtyRegion.IsEmpty() && !mPaintTask.IsPending()) {
@@ -335,7 +380,12 @@ PuppetWidget::DispatchEvent(WidgetGUIEvent* event, nsEventStatus& aStatus)
AutoCacheNativeKeyCommands autoCache(this);
if (event->mFlags.mIsSynthesizedForTests && !mNativeKeyCommandsValid) {
WidgetKeyboardEvent* keyEvent = event->AsKeyboardEvent();
if (keyEvent) {
// JIHAD: mTabChild is null in the standalone offscreen render daemon (there is no parent
// content process). Native key bindings are supplied by that parent, so when it is absent
// simply leave no cached native commands (fine for character input) rather than null-deref
// on RequestNativeKeyBindings — that deref SIGSEGV'd the daemon on every synthesized key
// event, which is exactly the crash that made typing into web inputs impossible.
if (keyEvent && mTabChild) {
mTabChild->RequestNativeKeyBindings(&autoCache, keyEvent);
}
}
@@ -583,7 +633,13 @@ PuppetWidget::GetLayerManager(PLayerTransactionChild* aShadowManager,
LayerManagerPersistence aPersistence)
{
if (!mLayerManager) {
mLayerManager = new ClientLayerManager(this);
if (JihadOffscreen()) {
// Jihad: no compositor/IPC — paint on this thread with a BasicLayerManager
// into mDrawTarget (see PuppetWidget::Paint LAYERS_BASIC branch).
mLayerManager = CreateBasicLayerManager();
} else {
mLayerManager = new ClientLayerManager(this);
}
}
ShadowLayerForwarder* lf = mLayerManager->AsShadowForwarder();
if (lf && !lf->HasShadowManager() && aShadowManager) {
@@ -602,6 +658,615 @@ PuppetWidget::RecreateLayerManager(PLayerTransactionChild* aShadowManager)
return mLayerManager;
}
// ---------------------------------------------------------------------------
// Jihad offscreen render support.
// ---------------------------------------------------------------------------
bool
PuppetWidget::JihadOffscreen()
{
static int sMode = -1;
if (sMode < 0) {
const char* e = getenv("JIHAD_OFFSCREEN");
sMode = (e && *e) ? 1 : 0;
}
return sMode == 1;
}
// Jihad headless: dimensions of the offscreen "screen", set by
// jihad_offscreen_create/resize and returned directly by PuppetScreen::GetRect.
// PuppetScreen must NOT ask hal for the screen config: the fallback hal
// implementation (hal/fallback/FallbackScreenConfiguration.cpp) resolves the
// config by querying the screen manager — i.e. PuppetScreen — which would
// recurse into hal again and overflow the stack.
static int32_t sJihadScreenWidth = 1024;
static int32_t sJihadScreenHeight = 768;
void
PuppetWidget::JihadEnsureDrawTarget()
{
int32_t w = mBounds.width > 0 ? mBounds.width : 1;
int32_t h = mBounds.height > 0 ? mBounds.height : 1;
if (mDrawTarget) {
IntSize have = mDrawTarget->GetSize();
if (have.width >= w && have.height >= h) {
return;
}
}
mDrawTarget = gfxPlatform::GetPlatform()->
CreateOffscreenContentDrawTarget(IntSize(w, h), SurfaceFormat::B8G8R8A8);
}
already_AddRefed<SourceSurface>
PuppetWidget::JihadSnapshot()
{
if (!mDrawTarget) {
return nullptr;
}
return mDrawTarget->Snapshot();
}
bool
PuppetWidget::JihadRenderDocument(nsIDocShell* aDocShell, double aZoom,
double aPanX, double aPanY)
{
if (!aDocShell) {
return false;
}
JihadEnsureDrawTarget();
if (!mDrawTarget) {
return false;
}
nsCOMPtr<nsIPresShell> presShell = aDocShell->GetPresShell();
if (!presShell) {
return false;
}
// Make sure layout is up to date before we snapshot the document.
presShell->FlushPendingNotifications(Flush_Layout);
int32_t w = mBounds.width > 0 ? mBounds.width : 1;
int32_t h = mBounds.height > 0 ? mBounds.height : 1;
// Clear the whole target to white first so any area RenderDocument does not cover
// (e.g. a viewport smaller than the buffer) reads white, not the initial black.
mDrawTarget->FillRect(mozilla::gfx::Rect(0, 0, mDrawTarget->GetSize().width,
mDrawTarget->GetSize().height),
mozilla::gfx::ColorPattern(mozilla::gfx::Color(1.f, 1.f, 1.f, 1.f)));
RefPtr<gfxContext> ctx = gfxContext::CreateOrNull(mDrawTarget);
if (!ctx) {
return false;
}
// Pinch/fit ZOOM (Jihad): magnify the capture by aZoom WITHOUT reflowing layout. Pre-scale
// the target context by Z and render only the 1/Z-reduced visible region, so w/Z x h/Z CSS
// px fill the w x h device buffer at Z x magnification. RenderDocument's own internal scale
// (AppUnitsPerDevPixel/AppUnitsPerCSSPixel) is 1 here (layout viewport left at the window
// width, no engine zoom), so the net paint scale is exactly Z. RenderDocument clips to the
// rect using the CURRENT (pre-scaled) matrix, so the clip lands at w x h device px. Z==1 is
// the identity path (unchanged). This is why SetFullZoom/SetResolution were wrong: they made
// the engine zoom, which RenderDocument's internal scale then cancels back into a 1/Z quadrant.
// RENDER_CARET draws the text caret when an editable is focused (Jihad: the on-screen
// keyboard needs a visible cursor).
double Z = (aZoom > 0.0) ? aZoom : 1.0;
uint32_t flags = nsIPresShell::RENDER_CARET;
nsRect r;
if (Z < 0.99 || Z > 1.01 || aPanX != 0.0 || aPanY != 0.0) {
// Zoomed / panned (|Z-1|>1%, matching the daemon's is-zoomed threshold so a near-1 fit-zoom
// like 1.0052 stays on the engine-scroll path and keeps window.onscroll/fixed/sticky correct):
// magnify by Z and render an ABSOLUTE DOCUMENT rect (aPanX,aPanY,w/Z,h/Z),
// IGNORING the engine scroll (RENDER_IGNORE_VIEWPORT_SCROLLING | RENDER_DOCUMENT_RELATIVE).
// This is a pure visual viewport: the whole page is reachable in BOTH axes with no display-
// list culling and no dependence on the engine scroll, so there is no blank-past-the-first-
// screen, no engine-parking echo, and zoom-OUT (Z<1, rect > viewport) is not clipped.
// RenderDocument clips to aRect.width/height in the (pre-scaled) matrix, so the clip lands at
// exactly w x h device px. RenderDocument's internal AppUnitsPerDevPixel/AppUnitsPerCSSPixel
// scale is 1 (no engine zoom — layout viewport stays device-width), so the net paint scale
// is exactly Z (zoom fix 2026-07-27; Codex review F2-F6 -> document-relative).
ctx->SetMatrix(ctx->CurrentMatrix().Scale(Z, Z));
flags |= nsIPresShell::RENDER_IGNORE_VIEWPORT_SCROLLING | nsIPresShell::RENDER_DOCUMENT_RELATIVE;
r = nsRect(nsPresContext::CSSPixelsToAppUnits((float)aPanX),
nsPresContext::CSSPixelsToAppUnits((float)aPanY),
nsPresContext::CSSPixelsToAppUnits((float)(w / Z)),
nsPresContext::CSSPixelsToAppUnits((float)(h / Z)));
} else {
// Unzoomed (Z==1, no pan): viewport-relative, the engine scroll drives position (so
// window.onscroll + position:fixed/sticky stay correct). Byte-identical to the pre-zoom-fix
// capture.
r = nsRect(0, 0,
nsPresContext::CSSPixelsToAppUnits((float)w),
nsPresContext::CSSPixelsToAppUnits((float)h));
}
nsresult rv = presShell->RenderDocument(r, flags, NS_RGB(255, 255, 255), ctx);
return NS_SUCCEEDED(rv);
}
// C entry points for the frozen-API render daemon. Exported from libxul so the
// daemon (which cannot see internal classes like PuppetWidget) can create an
// offscreen widget, hand it to nsIBaseWindow::InitWindow as the parent nsIWidget,
// drive a paint, and read the rendered ARGB32 pixels into its shared buffer.
extern "C" {
MOZ_EXPORT nsIWidget*
jihad_offscreen_create(int aWidth, int aHeight)
{
if (aWidth > 0) sJihadScreenWidth = aWidth;
if (aHeight > 0) sJihadScreenHeight = aHeight;
RefPtr<PuppetWidget> w = new PuppetWidget(nullptr);
LayoutDeviceIntRect bounds(0, 0, aWidth, aHeight);
nsWidgetInitData initData;
initData.mWindowType = eWindowType_toplevel;
w->Create(nullptr, nullptr, bounds, &initData);
w->Resize(aWidth, aHeight, false);
w->Show(true);
w->JihadEnsureDrawTarget();
return w.forget().take();
}
MOZ_EXPORT void
jihad_offscreen_resize(nsIWidget* aWidget, int aWidth, int aHeight)
{
if (aWidget) {
if (aWidth > 0) sJihadScreenWidth = aWidth;
if (aHeight > 0) sJihadScreenHeight = aHeight;
aWidget->Resize(aWidth, aHeight, true);
}
}
MOZ_EXPORT void
jihad_offscreen_paint(nsIWidget* aWidget)
{
if (aWidget) {
static_cast<PuppetWidget*>(aWidget)->PaintNowIfNeeded();
}
}
// True (and clears the flag) if content invalidated since the last call — layout
// invalidations from incremental page render, JS/SPA DOM updates, async image
// decode, CSS animation. The daemon polls this each tick and repaints on dirty,
// giving the offscreen embedding the engine-driven frame delivery the stock
// QtWebKit BrowserServer got from Qt paint events (BrowserPage::paintEvent).
MOZ_EXPORT bool
jihad_offscreen_take_dirty(nsIWidget* aWidget)
{
if (!aWidget) {
return false;
}
return static_cast<PuppetWidget*>(aWidget)->JihadTakeDirty();
}
// The bounding box of what changed, so the daemon can repaint only those rows. See
// PuppetWidget::JihadTakeDirtyRect. Returns false when nothing is dirty, leaving the outs
// untouched; a caller that ignores this simply keeps doing full-frame repaints.
MOZ_EXPORT bool
jihad_offscreen_take_dirty_rect(nsIWidget* aWidget, int32_t* aX, int32_t* aY,
int32_t* aW, int32_t* aH)
{
if (!aWidget) {
return false;
}
return static_cast<PuppetWidget*>(aWidget)->JihadTakeDirtyRect(aX, aY, aW, aH);
}
// Render the docShell's document into the widget's offscreen DrawTarget via the
// presShell (canonical offscreen render). The daemon passes the content nsIDocShell
// it already resolves. Returns true if the document rendered.
MOZ_EXPORT bool
jihad_offscreen_render_document_v2(nsIWidget* aWidget, nsIDocShell* aDocShell, double aZoom,
double aPanX, double aPanY)
{
// Versioned symbol (Codex F1): the zoom/pan args were added to an extern-C function, which has
// no parameter mangling — an old daemon vs new libxul (or vice versa) would pass garbage or
// silently drop the args. The rename makes any stale-binary mismatch fail LOUDLY at RTLD_NOW
// load (undefined symbol) instead of mis-rendering.
if (!aWidget) {
return false;
}
return static_cast<PuppetWidget*>(aWidget)->JihadRenderDocument(aDocShell, aZoom, aPanX, aPanY);
}
MOZ_EXPORT bool
jihad_offscreen_readback(nsIWidget* aWidget, void* aDest, int aStride,
int aWidth, int aHeight)
{
if (!aWidget || !aDest) {
return false;
}
PuppetWidget* pw = static_cast<PuppetWidget*>(aWidget);
// NOTE: do NOT call PaintNowIfNeeded() here — the widget Paint() path does not
// paint the embedded document into this offscreen widget (it clears mDrawTarget to
// black). The daemon renders the document via jihad_offscreen_render_document
// (presShell->RenderDocument) just before this readback; we only snapshot.
RefPtr<SourceSurface> snap = pw->JihadSnapshot();
if (!snap) {
return false;
}
RefPtr<DataSourceSurface> data = snap->GetDataSurface();
if (!data) {
return false;
}
size_t length;
int32_t srcStride;
mozilla::UniquePtr<char[]> buf =
nsContentUtils::GetSurfaceData(WrapNotNull(data.get()), &length, &srcStride);
if (!buf) {
return false;
}
IntSize sz = data->GetSize();
int rows = aHeight < sz.height ? aHeight : sz.height;
int rowBytes = (aWidth < sz.width ? aWidth : sz.width) * 4;
for (int y = 0; y < rows; ++y) {
memcpy(static_cast<uint8_t*>(aDest) + (size_t)y * aStride,
buf.get() + (size_t)y * srcStride, rowBytes);
}
return true;
}
// Render an ABSOLUTE document region straight into caller-owned memory (the
// daemon's shared buffer). Unlike jihad_offscreen_render_document_v2 this is not
// bounded by the widget's DrawTarget size, so the daemon can paint a region
// TALLER than the viewport (viewport + overscan) and give the adapter real pan
// headroom (scroll pan headroom fix 2026-08-02: painting exactly the viewport
// left zero headroom, so one pixel of pan exposed undrawn grey).
// aDocX/aDocY are CSS px (document-absolute); the region is aWidth x aHeight
// DEVICE px rendered at aZoom, i.e. it covers aWidth/Z x aHeight/Z CSS px.
// Always RENDER_DOCUMENT_RELATIVE + RENDER_IGNORE_VIEWPORT_SCROLLING: the engine
// scroll does not shift the output, and position:fixed content is NOT placed at
// the current scroll — at z~1 the daemon overlays a viewport-relative band
// (render_document_v2 + readback) over the visible rows to keep fixed/sticky
// correct on screen; the overscan strips only ever show transiently mid-pan.
MOZ_EXPORT bool
jihad_offscreen_render_region(nsIWidget* /*aWidget*/, nsIDocShell* aDocShell, double aZoom,
double aDocX, double aDocY, void* aDest, int aStride,
int aWidth, int aHeight)
{
if (!aDocShell || !aDest || aWidth <= 0 || aHeight <= 0 || aStride < aWidth * 4) {
return false;
}
nsCOMPtr<nsIPresShell> presShell = aDocShell->GetPresShell();
if (!presShell) {
return false;
}
presShell->FlushPendingNotifications(Flush_Layout);
RefPtr<DrawTarget> dt = Factory::CreateDrawTargetForData(
BackendType::CAIRO, static_cast<unsigned char*>(aDest),
IntSize(aWidth, aHeight), aStride, SurfaceFormat::B8G8R8A8);
if (!dt || !dt->IsValid()) {
return false;
}
// Opaque white base: RenderDocument's over-op on an opaque base keeps alpha at
// 255 everywhere, which the adapter's raw-word blit requires (no post-pass
// alpha forcing on this path — the pixels land directly in the shared buffer).
dt->FillRect(mozilla::gfx::Rect(0, 0, aWidth, aHeight),
mozilla::gfx::ColorPattern(mozilla::gfx::Color(1.f, 1.f, 1.f, 1.f)));
RefPtr<gfxContext> ctx = gfxContext::CreateOrNull(dt);
if (!ctx) {
return false;
}
double Z = (aZoom > 0.0) ? aZoom : 1.0;
ctx->SetMatrix(ctx->CurrentMatrix().Scale(Z, Z));
uint32_t flags = nsIPresShell::RENDER_CARET |
nsIPresShell::RENDER_IGNORE_VIEWPORT_SCROLLING |
nsIPresShell::RENDER_DOCUMENT_RELATIVE;
nsRect r(nsPresContext::CSSPixelsToAppUnits((float)aDocX),
nsPresContext::CSSPixelsToAppUnits((float)aDocY),
nsPresContext::CSSPixelsToAppUnits((float)(aWidth / Z)),
nsPresContext::CSSPixelsToAppUnits((float)(aHeight / Z)));
nsresult rv = presShell->RenderDocument(r, flags, NS_RGB(255, 255, 255), ctx);
if (NS_FAILED(rv)) {
return false;
}
// ENFORCE alpha == 255 over every pixel (adversarial review 2026-08-02 F3): the
// adapter raw-blits these words into the card surface, so any a<255 pixel is a
// see-through hole. The opaque-white base + OP_OVER argument holds on the paths
// we traced, but it is an engine-internal invariant across the whole display
// list — enforce it here rather than trust it (a few ms of byte stores).
dt->Flush();
for (int y = 0; y < aHeight; ++y) {
unsigned char* row = static_cast<unsigned char*>(aDest) + (size_t)y * aStride;
for (int x = 0; x < aWidth; ++x) {
row[(size_t)x * 4 + 3] = 0xff;
}
}
return true;
}
// Composite every OPEN XUL popup over an already-painted buffer.
//
// WHY THIS EXISTS. A popup (menupopup: the about:addons tools menu, context menus)
// is a SEPARATE DISPLAY ROOT — nsLayoutUtils::GetDisplayRootFrame returns the popup
// frame itself, not the document root — so RenderDocument on the content docShell
// never contains it, no matter how it is sized or positioned. On a desktop build the
// popup would be its own toplevel OS window and the window manager would put it on
// screen; headless there is no such window, so the pixels have to be composited by
// hand. The daemon calls this after its main paint, so a popup lands on top of the
// page exactly where the engine placed it.
//
// aOriginX/aOriginY are the document-space CSS coords of the buffer's top-left (the
// same origin the main render used) and aZoom the scale it rendered at, so a popup
// is placed with the identical transform as the content underneath it.
//
// Returns the number of popups composited (0 = nothing open, the overwhelmingly
// common case, and the reason this is cheap enough to call every frame).
MOZ_EXPORT int
jihad_offscreen_composite_popups(nsIWidget* /*aWidget*/, void* aDest, int aStride,
int aWidth, int aHeight, double aOriginX,
double aOriginY, double aZoom)
{
if (!aDest || aWidth <= 0 || aHeight <= 0 || aStride < aWidth * 4) {
return 0;
}
nsXULPopupManager* pm = nsXULPopupManager::GetInstance();
if (!pm) {
return 0;
}
nsTArray<nsIFrame*> popups;
pm->GetVisiblePopups(popups);
if (popups.IsEmpty()) {
return 0; // no popup open: pay nothing beyond this check
}
RefPtr<DrawTarget> dt = Factory::CreateDrawTargetForData(
BackendType::CAIRO, static_cast<unsigned char*>(aDest),
IntSize(aWidth, aHeight), aStride, SurfaceFormat::B8G8R8A8);
if (!dt || !dt->IsValid()) {
return 0;
}
const double Z = (aZoom > 0.0) ? aZoom : 1.0;
int painted = 0;
// GetVisiblePopups walks the chain innermost-first; paint in reverse so a submenu
// lands on top of the menu that opened it.
for (int32_t i = popups.Length() - 1; i >= 0; --i) {
nsIFrame* frame = popups[i];
if (!frame) {
continue;
}
// The popup's own rect, in the same document space the buffer was rendered in.
// GetScreenRectInAppUnits() is what the widget code positions the popup window
// with, and in this embedding the "screen" IS the document surface: PuppetScreen
// reports the offscreen bounds, so screen coords and document coords agree.
nsRect screen = frame->GetScreenRectInAppUnits();
const double cssX = nsPresContext::AppUnitsToFloatCSSPixels(screen.x);
const double cssY = nsPresContext::AppUnitsToFloatCSSPixels(screen.y);
const double cssW = nsPresContext::AppUnitsToFloatCSSPixels(screen.width);
const double cssH = nsPresContext::AppUnitsToFloatCSSPixels(screen.height);
if (cssW <= 0 || cssH <= 0) {
// A popup that is open but has no size is the "0x0 popup" failure this whole
// path was built for — say so rather than silently drawing nothing.
if (getenv("JIHAD_OFFSCREEN")) {
fprintf(stderr, "[jihad-popup] composite: frame=%p is OPEN but %gx%g — skipped\n",
(void*)frame, cssW, cssH);
}
continue;
}
// Device px in the destination buffer.
const double dx = (cssX - aOriginX) * Z;
const double dy = (cssY - aOriginY) * Z;
if (dx >= aWidth || dy >= aHeight || dx + cssW * Z <= 0 || dy + cssH * Z <= 0) {
continue; // entirely off this buffer
}
RefPtr<gfxContext> ctx = gfxContext::CreateOrNull(dt);
if (!ctx) {
continue;
}
// Clip to the popup's own box so a display list that paints outside its bounds
// (shadows, overflow) cannot scribble over the page around it.
ctx->Save();
ctx->Clip(mozilla::gfx::Rect(dx, dy, cssW * Z, cssH * Z));
// cairo semantics: each call pre-multiplies, i.e. it applies FIRST in user
// space. Scale then translate => device = css * Z + (dx, dy), which places the
// popup's own coordinate space at its spot in this buffer.
ctx->SetMatrix(ctx->CurrentMatrix().Translate(gfxPoint(dx, dy)).Scale(Z, Z));
nsRenderingContext rc(ctx);
// Frame-relative dirty region covering the whole popup. PAINT_DOCUMENT_RELATIVE
// is deliberately NOT set: we want the popup drawn at its own origin, which the
// transform above has already placed.
nsRect dirty(nsPoint(0, 0), screen.Size());
nsresult rv = nsLayoutUtils::PaintFrame(
&rc, frame, nsRegion(dirty), NS_RGBA(0, 0, 0, 0),
nsDisplayListBuilderMode::PAINTING,
nsLayoutUtils::PaintFrameFlags::PAINT_SYNC_DECODE_IMAGES |
nsLayoutUtils::PaintFrameFlags::PAINT_IGNORE_SUPPRESSION |
nsLayoutUtils::PaintFrameFlags::PAINT_HIDE_CARET);
ctx->Restore();
if (NS_FAILED(rv)) {
if (getenv("JIHAD_OFFSCREEN")) {
fprintf(stderr, "[jihad-popup] composite: PaintFrame failed frame=%p rv=0x%x\n",
(void*)frame, (unsigned)rv);
}
continue;
}
++painted;
if (getenv("JIHAD_MENU_DEBUG")) {
// Which row does the engine consider active, and does it carry the attribute the
// highlight rule keys on? Without this, "no highlight" cannot be told apart from
// "no active item" — the two look identical on screen.
nsMenuPopupFrame* mpf = do_QueryFrame(frame);
nsMenuFrame* cur = mpf ? mpf->GetCurrentMenuItem() : nullptr;
nsIContent* c = cur ? cur->GetContent() : nullptr;
nsAutoString act;
if (c) c->GetAttr(kNameSpaceID_None, nsGkAtoms::menuactive, act);
fprintf(stderr, "[jihad-popup] active item=%p content=%p _moz-menuactive=[%s]\n",
(void*)cur, (void*)c, NS_ConvertUTF16toUTF8(act).get());
}
if (getenv("JIHAD_OFFSCREEN")) {
fprintf(stderr, "[jihad-popup] composite: frame=%p at %g,%g %gx%g (dev %g,%g z=%g)\n",
(void*)frame, cssX, cssY, cssW, cssH, dx, dy, Z);
}
}
if (painted) {
// Same alpha invariant the region render enforces: the adapter raw-blits these
// words into the card surface, so a popup pixel with a<255 is a see-through hole.
dt->Flush();
for (int y = 0; y < aHeight; ++y) {
unsigned char* row = static_cast<unsigned char*>(aDest) + (size_t)y * aStride;
for (int x = 0; x < aWidth; ++x) {
row[(size_t)x * 4 + 3] = 0xff;
}
}
}
return painted;
}
// How many popups are open right now. The daemon uses this straight after dispatching a
// tap to tell "that tap opened a menu" from "it did not", which is what lets it drop the
// DUPLICATE delivery of the same physical tap instead of letting it close the menu again.
MOZ_EXPORT int
jihad_offscreen_popup_count()
{
nsXULPopupManager* pm = nsXULPopupManager::GetInstance();
if (!pm) {
return 0;
}
nsTArray<nsIFrame*> popups;
pm->GetVisiblePopups(popups);
return (int)popups.Length();
}
// Close every open popup ("roll up").
//
// On a desktop build this happens by itself: the popup's widget takes a native mouse
// capture, so a click anywhere else reaches the popup manager as a rollup. There is no
// native capture in this headless embedding — the daemon synthesises taps straight into
// the content document's presShell — so tapping outside a menu left it open (measured;
// it closed only sometimes, depending on what the tap happened to hit). The daemon calls
// this explicitly for a tap that lands outside every popup, which is what a user means.
MOZ_EXPORT bool
jihad_offscreen_popup_rollup()
{
nsXULPopupManager* pm = nsXULPopupManager::GetInstance();
if (!pm) {
return false;
}
nsTArray<nsIFrame*> popups;
pm->GetVisiblePopups(popups);
if (popups.IsEmpty()) {
return false;
}
// aCount 0 = roll up the whole chain; aFlush = run the close synchronously so the next
// paint no longer contains the popup.
bool rolled = pm->Rollup(0, true, nullptr, nullptr);
if (getenv("JIHAD_OFFSCREEN")) {
fprintf(stderr, "[jihad-popup] rollup: closed=%d (%u were open)\n",
(int)rolled, (unsigned)popups.Length());
}
return rolled;
}
// Route a mouse event into an OPEN popup.
//
// The same separate-display-root property that keeps a popup out of the main render
// also keeps it out of the main document's hit-testing: the daemon's normal input path
// resolves a tap with elementFromPoint on the CONTENT document, which walks the content
// display root and cannot see popup frames. Measured: with the tools menu open, a tap on
// its first row resolved to the <vbox> UNDER the menu. So a popup that is visible is not
// operable unless its events are dispatched against its own display root, which is what
// this does.
//
// aX/aY are document CSS px (same space the composite places the popup in). Returns true
// if a visible popup contained the point and the event was dispatched to it — the caller
// then does NOT run its normal content-document dispatch for that tap.
// aMsg: 0 = move, 1 = down, 2 = up.
MOZ_EXPORT bool
jihad_offscreen_popup_mouse(int aMsg, double aX, double aY)
{
nsXULPopupManager* pm = nsXULPopupManager::GetInstance();
if (!pm) {
return false;
}
nsTArray<nsIFrame*> popups;
pm->GetVisiblePopups(popups);
if (popups.IsEmpty()) {
return false;
}
const nscoord appX = nsPresContext::CSSPixelsToAppUnits((float)aX);
const nscoord appY = nsPresContext::CSSPixelsToAppUnits((float)aY);
// Innermost first: GetVisiblePopups walks the chain from the deepest submenu out, so
// the first hit is the one the user is actually pointing at.
for (uint32_t i = 0; i < popups.Length(); ++i) {
nsIFrame* frame = popups[i];
if (!frame) {
continue;
}
nsRect screen = frame->GetScreenRectInAppUnits();
if (screen.width <= 0 || screen.height <= 0 ||
appX < screen.x || appX >= screen.XMost() ||
appY < screen.y || appY >= screen.YMost()) {
continue;
}
nsIPresShell* presShell = frame->PresContext()->PresShell();
if (!presShell) {
return false;
}
// The event's refPoint is widget-relative; a popup's own widget has the popup's
// top-left as its origin, so subtract the popup's screen position.
nsIWidget* widget = frame->GetNearestWidget();
if (!widget) {
return false;
}
const int32_t localX = nsPresContext::AppUnitsToIntCSSPixels(appX - screen.x);
const int32_t localY = nsPresContext::AppUnitsToIntCSSPixels(appY - screen.y);
EventMessage msg = (aMsg == 1) ? eMouseDown : (aMsg == 2) ? eMouseUp : eMouseMove;
WidgetMouseEvent event(true, msg, widget, WidgetMouseEvent::eReal);
event.mRefPoint = LayoutDeviceIntPoint(localX, localY);
event.mClickCount = (msg == eMouseMove) ? 0 : 1;
event.button = WidgetMouseEvent::eLeftButton;
if (msg != eMouseMove) {
event.mFlags.mIsSynthesizedForTests = false;
}
nsEventStatus status = nsEventStatus_eIgnore;
presShell->HandleEvent(frame, &event, false, &status);
if (getenv("JIHAD_OFFSCREEN")) {
fprintf(stderr, "[jihad-popup] mouse msg=%d doc=%g,%g -> popup-local %d,%d status=%d\n",
aMsg, aX, aY, localX, localY, (int)status);
}
return true;
}
return false;
}
MOZ_EXPORT void
jihad_offscreen_release(nsIWidget* aWidget)
{
if (aWidget) {
PuppetWidget* pw = static_cast<PuppetWidget*>(aWidget);
pw->Destroy();
pw->Release();
}
}
// Force PSM/NSS (TLS) to initialize on the CURRENT (main) thread. The frozen-API
// render daemon can't resolve the PSM contract via do_GetService, but this internal
// libxul code can (EnsureNSSInitializedChromeOrContent -> do_GetService(PSM)). Without
// this, the first https:// request lazily constructs nsNSSComponent from the necko
// socket thread, whose ctor does MOZ_RELEASE_ASSERT(NS_IsMainThread()) -> daemon abort
// ("loads forever"). The daemon calls this on its main thread before an https load.
MOZ_EXPORT bool
jihad_init_nss()
{
// Per the crash backtrace: on the first https:// load, necko's SOCKET thread does
// nsSocketTransport::Init -> GetSocketProvider("ssl") -> nsSSLSocketProviderConstructor
// -> EnsureNSSInitialized -> do_GetService(PSM) -> nsNSSComponent ctor, which asserts
// NS_IsMainThread() and crashes. Direct do_GetService(PSM) fails on the main thread
// (PSM registers only when the ssl socket provider is first built). So build the SSL
// socket provider HERE on the main thread — it runs the exact same EnsureNSSInitialized
// chain, constructing nsNSSComponent on the main thread; the socket thread then reuses
// the singleton and no longer constructs it. (Idempotent — cached after first call.)
nsresult rv = NS_ERROR_FAILURE;
nsCOMPtr<nsISupports> ssl =
do_GetService("@mozilla.org/network/socket;2?type=ssl", &rv);
nsCOMPtr<nsISupports> tls =
do_GetService("@mozilla.org/network/socket;2?type=starttls");
fprintf(stderr, "[jihad-bs] jihad_init_nss(ssl provider): main=%d rv=0x%x got=%p\n",
(int)NS_IsMainThread(), (unsigned)rv, (void*)ssl.get());
return NS_SUCCEEDED(rv);
}
} // extern "C"
nsresult
PuppetWidget::RequestIMEToCommitComposition(bool aCancel)
{
@@ -1086,13 +1751,25 @@ PuppetWidget::Paint()
mTabChild->NotifyPainted();
}
} else if (mozilla::layers::LayersBackend::LAYERS_BASIC == mLayerManager->GetBackendType()) {
if (JihadOffscreen()) {
JihadEnsureDrawTarget();
}
RefPtr<gfxContext> ctx = gfxContext::CreateOrNull(mDrawTarget);
if (!ctx) {
gfxDevCrash(LogReason::InvalidContext) << "PuppetWidget context problem " << gfx::hexa(mDrawTarget);
return NS_ERROR_FAILURE;
}
ctx->Rectangle(gfxRect(0,0,0,0));
ctx->Clip();
if (JihadOffscreen()) {
// Jihad offscreen render: paint the FULL widget area into mDrawTarget. The
// stock path below clips to an EMPTY rect because in the content process the
// real pixels come from the compositor and this BASIC branch intentionally
// draws nothing — but we bypass the compositor and need the actual pixels.
ctx->Rectangle(gfxRect(0, 0, mBounds.width, mBounds.height));
ctx->Clip();
} else {
ctx->Rectangle(gfxRect(0,0,0,0));
ctx->Clip();
}
AutoLayerManagerSetup setupLayerManager(this, ctx,
BufferMode::BUFFER_NONE);
GetCurrentWidgetListener()->PaintWindow(this, region);
@@ -1349,6 +2026,16 @@ NS_IMETHODIMP
PuppetScreen::GetRect(int32_t *outLeft, int32_t *outTop,
int32_t *outWidth, int32_t *outHeight)
{
if (PuppetWidget::JihadOffscreen()) {
// Answer directly from the stored offscreen dimensions. Going through
// ScreenConfig()/hal here would recurse back into the fallback hal, which
// queries this same screen manager, overflowing the stack.
*outLeft = 0;
*outTop = 0;
*outWidth = sJihadScreenWidth;
*outHeight = sJihadScreenHeight;
return NS_OK;
}
nsIntRect r = ScreenConfig().rect();
*outLeft = r.x;
*outTop = r.y;
@@ -1367,6 +2054,11 @@ PuppetScreen::GetAvailRect(int32_t *outLeft, int32_t *outTop,
NS_IMETHODIMP
PuppetScreen::GetPixelDepth(int32_t *aPixelDepth)
{
if (PuppetWidget::JihadOffscreen()) {
// Fixed depth; ScreenConfig()/hal would recurse back into this object.
*aPixelDepth = 24;
return NS_OK;
}
*aPixelDepth = ScreenConfig().pixelDepth();
return NS_OK;
}
@@ -1374,6 +2066,10 @@ PuppetScreen::GetPixelDepth(int32_t *aPixelDepth)
NS_IMETHODIMP
PuppetScreen::GetColorDepth(int32_t *aColorDepth)
{
if (PuppetWidget::JihadOffscreen()) {
*aColorDepth = 24;
return NS_OK;
}
*aColorDepth = ScreenConfig().colorDepth();
return NS_OK;
}
@@ -1452,8 +2148,23 @@ PuppetScreenManager::GetSystemDefaultScale(float *aDefaultScale)
nsIWidgetListener*
PuppetWidget::GetCurrentWidgetListener()
{
if (!mPreviouslyAttachedWidgetListener ||
!mAttachedWidgetListener) {
// JIHAD: fall back to mWidgetListener when nothing is ATTACHED, exactly as the real
// widget does (widget/gtk/nsWindow.cpp GetListener:
// `mAttachedWidgetListener ? mAttachedWidgetListener : mWidgetListener`). PuppetWidget
// was written for the content-process case, where the view is always attached to a
// top-level widget, so it only ever consulted the attached listener — and in this
// standalone offscreen embedding nothing attaches, leaving mAttachedWidgetListener null.
//
// Everything routed through DispatchEvent was therefore silently DROPPED. Mouse input
// hid the problem because nsIDOMWindowUtils has `…SendMouseEventToWindow`, which goes to
// the presShell directly; there is no `SendKeyEventToWindow`, so SendKeyEvent went
// widget->DispatchEvent and vanished. That is why text could only be inserted through
// the editor (SetValue/SetSelectionRange) and why anything reacting to real key events —
// XUL <key> elements, tree type-ahead, Escape/Enter handling — got nothing at all.
if (!mAttachedWidgetListener) {
return mWidgetListener;
}
if (!mPreviouslyAttachedWidgetListener) {
return mAttachedWidgetListener;
}
+73
View File
@@ -25,6 +25,8 @@
#include "mozilla/ContentCache.h"
#include "mozilla/EventForwards.h"
class nsIDocShell; // Jihad offscreen render (JihadRenderDocument)
namespace mozilla {
namespace dom {
@@ -200,6 +202,72 @@ public:
// Paint the widget immediately if any paints are queued up.
void PaintNowIfNeeded();
// --- Jihad offscreen render support ---
// When the env var JIHAD_OFFSCREEN is set, a (null-TabChild) PuppetWidget
// renders through an in-process CPU BasicLayerManager into an in-memory
// DrawTarget (no compositor, no native window, no X/GTK), so a headless
// render daemon can read the pixels straight out. See the extern "C"
// jihad_offscreen_* entry points in PuppetWidget.cpp.
static bool JihadOffscreen();
void JihadEnsureDrawTarget();
already_AddRefed<mozilla::gfx::SourceSurface> JihadSnapshot();
// Render the docShell's document straight into mDrawTarget via the presShell
// (the canonical offscreen path, like canvas drawWindow / tab thumbnails). The
// widget Paint() path does NOT paint the embedded content into this widget, so
// relying on it produced a black frame; this renders the actual document.
bool JihadRenderDocument(nsIDocShell* aDocShell, double aZoom = 1.0,
double aPanX = 0.0, double aPanY = 0.0);
// Sticky invalidation flag for the render daemon (jihad_offscreen_take_dirty):
// set on every Invalidate() so the daemon knows CONTENT CHANGED and a repaint is
// due — the offscreen embedding has no compositor/paint-event feedback loop, so
// without this the daemon's frame delivery is input-driven only and JS/SPA DOM
// updates, async image decode, and incremental page render leave the shared
// buffer stale (the on-device "old page sticks around until you drag" bug).
// Deliberately separate from mDirtyRegion, which the PaintTask self-clears.
//
// DRAINS THE CHILD CHAIN: the embedding's view hierarchy does not paint through
// the top-level widget the daemon created — nsView::CreateWidgetForParent goes
// through PuppetWidget::CreateChild, which makes a SEPARATE child PuppetWidget
// (linked back via SetChild). Every Invalidate() from the view manager lands on
// that CHILD, setting the child's mJihadDirty, while the daemon polls the
// top-level parent — whose flag nothing ever set. That inert dirty loop is why
// anything that became visually ready after the last load/input-driven paint
// (async image decode — the about:addons icons — JS/SPA DOM updates) never
// appeared on the device: no repaint was ever triggered. Draining the chain
// here makes the poll see the child's invalidations.
bool JihadTakeDirty() {
bool d = mJihadDirty; mJihadDirty = false;
for (PuppetWidget* c = mChild.get(); c; c = c->mChild.get()) {
if (c->mJihadDirty) { d = true; c->mJihadDirty = false; }
}
return d;
}
// The BOUNDING BOX of everything invalidated since the last drain, in the widget's own
// device px, drained with the flag. A whole-viewport software repaint costs ~40 ms on this
// hardware, which caps anything animated at ~20 fps no matter how fast the source runs; a
// plugin animating in a 320x240 box only needs those rows. Accumulated as a bounding box
// rather than a region on purpose: the consumer can only issue rectangular renders, so a
// precise region would be discarded at the first use anyway.
bool JihadTakeDirtyRect(int32_t* aX, int32_t* aY, int32_t* aW, int32_t* aH) {
LayoutDeviceIntRect r = mJihadDirtyRect;
mJihadDirtyRect.SetEmpty();
for (PuppetWidget* c = mChild.get(); c; c = c->mChild.get()) {
if (!c->mJihadDirtyRect.IsEmpty()) {
r = r.IsEmpty() ? c->mJihadDirtyRect : r.Union(c->mJihadDirtyRect);
c->mJihadDirtyRect.SetEmpty();
}
}
if (r.IsEmpty()) {
return false;
}
if (aX) *aX = r.x;
if (aY) *aY = r.y;
if (aW) *aW = r.width;
if (aH) *aH = r.height;
return true;
}
virtual TabChild* GetOwningTabChild() override { return mTabChild; }
void UpdateBackingScaleCache(float aDpi, int32_t aRounding, double aScale)
@@ -342,6 +410,11 @@ private:
// event handler.
RefPtr<PuppetWidget> mChild;
LayoutDeviceIntRegion mDirtyRegion;
bool mJihadDirty = false; // sticky Invalidate() flag, drained by JihadTakeDirty()
// Bounding box of the same invalidations, drained by JihadTakeDirtyRect(). Kept beside the
// flag rather than replacing it: the flag answers "is anything dirty" for callers that do a
// full repaint, and must keep working unchanged if the rect is ignored.
LayoutDeviceIntRect mJihadDirtyRect;
nsRevocableEventPtr<PaintTask> mPaintTask;
RefPtr<MemoryPressureObserver> mMemoryPressureObserver;
// XXX/cjones: keeping this around until we teach LayerManager to do
+32 -18
View File
@@ -87,12 +87,26 @@ moz_gtk_enable_style_props(style_prop_t styleGetProp)
return MOZ_GTK_SUCCESS;
}
/* Jihad: realizing a widget creates a GdkWindow, which requires an X display.
* The headless (JIHAD_OFFSCREEN, on-device) daemon has no X server; it only
* queries widget STYLES for theme metrics/colors (which work on unrealized
* widgets) and never DRAWS native controls (disabled via
* nsNativeThemeGTK::ThemeSupportsWidget). Skip realization when there is no
* display so these queries don't crash in gtk_widget_realize -> gdk_window_new. */
static void
moz_gtk_maybe_realize(GtkWidget* aWidget)
{
if (gdk_screen_get_default()) {
gtk_widget_realize(aWidget);
}
}
static gint
ensure_window_widget()
{
if (!gProtoWindow) {
gProtoWindow = gtk_window_new(GTK_WINDOW_POPUP);
gtk_widget_realize(gProtoWindow);
moz_gtk_maybe_realize(gProtoWindow);
moz_gtk_set_widget_name(gProtoWindow);
}
return MOZ_GTK_SUCCESS;
@@ -108,7 +122,7 @@ setup_widget_prototype(GtkWidget* widget)
}
gtk_container_add(GTK_CONTAINER(gProtoLayout), widget);
gtk_widget_realize(widget);
moz_gtk_maybe_realize(widget);
g_object_set_data(G_OBJECT(widget), "transparent-bg-hint", GINT_TO_POINTER(TRUE));
return MOZ_GTK_SUCCESS;
}
@@ -163,7 +177,7 @@ ensure_button_arrow_widget()
gButtonArrowWidget = gtk_arrow_new(GTK_ARROW_DOWN, GTK_SHADOW_OUT);
gtk_container_add(GTK_CONTAINER(gToggleButtonWidget), gButtonArrowWidget);
gtk_widget_realize(gButtonArrowWidget);
moz_gtk_maybe_realize(gButtonArrowWidget);
}
return MOZ_GTK_SUCCESS;
}
@@ -253,7 +267,7 @@ moz_gtk_get_combo_box_inner_button(GtkWidget *widget, gpointer client_data)
gComboBoxButtonWidget = widget;
g_object_add_weak_pointer(G_OBJECT(widget),
(gpointer) &gComboBoxButtonWidget);
gtk_widget_realize(widget);
moz_gtk_maybe_realize(widget);
g_object_set_data(G_OBJECT(widget), "transparent-bg-hint", GINT_TO_POINTER(TRUE));
}
}
@@ -272,7 +286,7 @@ moz_gtk_get_combo_box_button_inner_widgets(GtkWidget *widget,
(gpointer) &gComboBoxArrowWidget);
} else
return;
gtk_widget_realize(widget);
moz_gtk_maybe_realize(widget);
g_object_set_data(G_OBJECT(widget), "transparent-bg-hint", GINT_TO_POINTER(TRUE));
}
@@ -312,7 +326,7 @@ ensure_combo_box_widgets()
gComboBoxArrowWidget = buttonChild;
g_object_add_weak_pointer(G_OBJECT(buttonChild), (gpointer)
&gComboBoxArrowWidget);
gtk_widget_realize(gComboBoxArrowWidget);
moz_gtk_maybe_realize(gComboBoxArrowWidget);
g_object_set_data(G_OBJECT(gComboBoxArrowWidget),
"transparent-bg-hint", GINT_TO_POINTER(TRUE));
}
@@ -363,7 +377,7 @@ moz_gtk_get_combo_box_entry_inner_widgets(GtkWidget *widget,
(gpointer) &gComboBoxEntryTextareaWidget);
} else
return;
gtk_widget_realize(widget);
moz_gtk_maybe_realize(widget);
g_object_set_data(G_OBJECT(widget), "transparent-bg-hint", GINT_TO_POINTER(TRUE));
}
@@ -374,7 +388,7 @@ moz_gtk_get_combo_box_entry_arrow(GtkWidget *widget, gpointer client_data)
gComboBoxEntryArrowWidget = widget;
g_object_add_weak_pointer(G_OBJECT(widget),
(gpointer) &gComboBoxEntryArrowWidget);
gtk_widget_realize(widget);
moz_gtk_maybe_realize(widget);
g_object_set_data(G_OBJECT(widget), "transparent-bg-hint", GINT_TO_POINTER(TRUE));
}
}
@@ -425,7 +439,7 @@ ensure_combo_box_entry_widgets()
gComboBoxEntryArrowWidget = buttonChild;
g_object_add_weak_pointer(G_OBJECT(buttonChild), (gpointer)
&gComboBoxEntryArrowWidget);
gtk_widget_realize(gComboBoxEntryArrowWidget);
moz_gtk_maybe_realize(gComboBoxEntryArrowWidget);
g_object_set_data(G_OBJECT(gComboBoxEntryArrowWidget),
"transparent-bg-hint", GINT_TO_POINTER(TRUE));
}
@@ -466,7 +480,7 @@ ensure_toolbar_widget()
ensure_handlebox_widget();
gToolbarWidget = gtk_toolbar_new();
gtk_container_add(GTK_CONTAINER(gHandleBoxWidget), gToolbarWidget);
gtk_widget_realize(gToolbarWidget);
moz_gtk_maybe_realize(gToolbarWidget);
g_object_set_data(G_OBJECT(gToolbarWidget), "transparent-bg-hint", GINT_TO_POINTER(TRUE));
}
return MOZ_GTK_SUCCESS;
@@ -488,7 +502,7 @@ ensure_tooltip_widget()
{
if (!gTooltipWidget) {
gTooltipWidget = gtk_window_new(GTK_WINDOW_POPUP);
gtk_widget_realize(gTooltipWidget);
moz_gtk_maybe_realize(gTooltipWidget);
moz_gtk_set_widget_name(gTooltipWidget);
}
return MOZ_GTK_SUCCESS;
@@ -531,7 +545,7 @@ ensure_frame_widget()
ensure_statusbar_widget();
gFrameWidget = gtk_frame_new(NULL);
gtk_container_add(GTK_CONTAINER(gStatusbarWidget), gFrameWidget);
gtk_widget_realize(gFrameWidget);
moz_gtk_maybe_realize(gFrameWidget);
}
return MOZ_GTK_SUCCESS;
}
@@ -554,7 +568,7 @@ ensure_menu_bar_item_widget()
gMenuBarItemWidget = gtk_menu_item_new();
gtk_menu_shell_append(GTK_MENU_SHELL(gMenuBarWidget),
gMenuBarItemWidget);
gtk_widget_realize(gMenuBarItemWidget);
moz_gtk_maybe_realize(gMenuBarItemWidget);
g_object_set_data(G_OBJECT(gMenuBarItemWidget),
"transparent-bg-hint", GINT_TO_POINTER(TRUE));
}
@@ -569,7 +583,7 @@ ensure_menu_popup_widget()
gMenuPopupWidget = gtk_menu_new();
gtk_menu_item_set_submenu(GTK_MENU_ITEM(gMenuBarItemWidget),
gMenuPopupWidget);
gtk_widget_realize(gMenuPopupWidget);
moz_gtk_maybe_realize(gMenuPopupWidget);
g_object_set_data(G_OBJECT(gMenuPopupWidget),
"transparent-bg-hint", GINT_TO_POINTER(TRUE));
}
@@ -584,7 +598,7 @@ ensure_menu_item_widget()
gMenuItemWidget = gtk_menu_item_new_with_label("M");
gtk_menu_shell_append(GTK_MENU_SHELL(gMenuPopupWidget),
gMenuItemWidget);
gtk_widget_realize(gMenuItemWidget);
moz_gtk_maybe_realize(gMenuItemWidget);
g_object_set_data(G_OBJECT(gMenuItemWidget),
"transparent-bg-hint", GINT_TO_POINTER(TRUE));
}
@@ -599,7 +613,7 @@ ensure_image_menu_item_widget()
gImageMenuItemWidget = gtk_image_menu_item_new();
gtk_menu_shell_append(GTK_MENU_SHELL(gMenuPopupWidget),
gImageMenuItemWidget);
gtk_widget_realize(gImageMenuItemWidget);
moz_gtk_maybe_realize(gImageMenuItemWidget);
g_object_set_data(G_OBJECT(gImageMenuItemWidget),
"transparent-bg-hint", GINT_TO_POINTER(TRUE));
}
@@ -614,7 +628,7 @@ ensure_menu_separator_widget()
gMenuSeparatorWidget = gtk_separator_menu_item_new();
gtk_menu_shell_append(GTK_MENU_SHELL(gMenuPopupWidget),
gMenuSeparatorWidget);
gtk_widget_realize(gMenuSeparatorWidget);
moz_gtk_maybe_realize(gMenuSeparatorWidget);
g_object_set_data(G_OBJECT(gMenuSeparatorWidget),
"transparent-bg-hint", GINT_TO_POINTER(TRUE));
}
@@ -629,7 +643,7 @@ ensure_check_menu_item_widget()
gCheckMenuItemWidget = gtk_check_menu_item_new_with_label("M");
gtk_menu_shell_append(GTK_MENU_SHELL(gMenuPopupWidget),
gCheckMenuItemWidget);
gtk_widget_realize(gCheckMenuItemWidget);
moz_gtk_maybe_realize(gCheckMenuItemWidget);
g_object_set_data(G_OBJECT(gCheckMenuItemWidget),
"transparent-bg-hint", GINT_TO_POINTER(TRUE));
}
+4 -1
View File
@@ -3,4 +3,7 @@
# 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/.
DIRS += ['stub', 'gtk2', 'gtk3']
DIRS += ['stub', 'gtk3']
if CONFIG['MOZ_ENABLE_NPAPI_GTK2']:
DIRS += ['gtk2']
+4 -1
View File
@@ -7,9 +7,12 @@ SOURCES += [
'../mozgtk.c',
]
for var in ('COMMON_SYMBOLS', 'GTK2_SYMBOLS', 'GTK3_SYMBOLS'):
for var in ('COMMON_SYMBOLS', 'GTK3_SYMBOLS'):
DEFINES[var] = True
if CONFIG['MOZ_ENABLE_NPAPI_GTK2']:
DEFINES['GTK2_SYMBOLS'] = True
SharedLibrary('mozgtk_stub')
SONAME = 'mozgtk'
+15
View File
@@ -1060,6 +1060,21 @@ nsLookAndFeel::Init()
GdkColor colorValue;
GdkColor *colorValuePtr;
// Jihad: headless (no X display, JIHAD_OFFSCREEN daemon on the device). The
// GTK theme cannot be queried; every gtk_invisible_new()/gtk_widget_realize()
// below would crash (gdk_window_new asserts 'GDK_IS_SCREEN'/'GDK_IS_WINDOW'
// and then a null-deref). Provide a default GtkStyle so GetColor()'s mStyle
// reads stay valid, and skip all widget realization. System colours fall back
// to GTK defaults; CSS-styled web content does not depend on them.
if (!gdk_display_get_default()) {
#if (MOZ_WIDGET_GTK == 2)
if (!mStyle) {
mStyle = gtk_style_new();
}
#endif
return;
}
#if (MOZ_WIDGET_GTK == 2)
NS_ASSERTION(!mStyle, "already initialized");
// GtkInvisibles come with a refcount that is not floating
+8
View File
@@ -1838,6 +1838,14 @@ nsNativeThemeGTK::ThemeSupportsWidget(nsPresContext* aPresContext,
nsIFrame* aFrame,
uint8_t aWidgetType)
{
// Jihad: headless (no X display, JIHAD_OFFSCREEN daemon on the device). Native
// GTK theming draws through gtk2drawing.c, which realizes GTK widgets
// (ensure_window_widget -> gtk_widget_realize -> gdk_window_new) and crashes
// without a display. Report no native-theme support so Gecko falls back to its
// own non-native control rendering. Controls look non-native; content is fine.
if (!gdk_display_get_default())
return false;
if (IsWidgetTypeDisabled(mDisabledWidgetTypes, aWidgetType))
return false;
+80
View File
@@ -0,0 +1,80 @@
/* -*- Mode: C++; tab-width: 2; 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 "GfxInfo.h"
#include "nsUnicharUtils.h"
namespace mozilla {
namespace widget {
#ifdef DEBUG
NS_IMPL_ISUPPORTS_INHERITED(GfxInfo, GfxInfoBase, nsIGfxInfoDebug)
#endif
// No Direct2D / DirectWrite on a headless unix target.
nsresult GfxInfo::GetD2DEnabled(bool *aEnabled) { return NS_ERROR_FAILURE; }
nsresult GfxInfo::GetDWriteEnabled(bool *aEnabled) { return NS_ERROR_FAILURE; }
NS_IMETHODIMP GfxInfo::GetDWriteVersion(nsAString&) { return NS_ERROR_FAILURE; }
NS_IMETHODIMP GfxInfo::GetCleartypeParameters(nsAString&) { return NS_ERROR_FAILURE; }
// No adapter to describe in a headless build.
NS_IMETHODIMP GfxInfo::GetAdapterDescription(nsAString&) { return NS_ERROR_FAILURE; }
NS_IMETHODIMP GfxInfo::GetAdapterDescription2(nsAString&) { return NS_ERROR_FAILURE; }
NS_IMETHODIMP GfxInfo::GetAdapterDriver(nsAString&) { return NS_ERROR_FAILURE; }
NS_IMETHODIMP GfxInfo::GetAdapterDriver2(nsAString&) { return NS_ERROR_FAILURE; }
NS_IMETHODIMP GfxInfo::GetAdapterVendorID(nsAString&) { return NS_ERROR_FAILURE; }
NS_IMETHODIMP GfxInfo::GetAdapterVendorID2(nsAString&) { return NS_ERROR_FAILURE; }
NS_IMETHODIMP GfxInfo::GetAdapterDeviceID(nsAString&) { return NS_ERROR_FAILURE; }
NS_IMETHODIMP GfxInfo::GetAdapterDeviceID2(nsAString&) { return NS_ERROR_FAILURE; }
NS_IMETHODIMP GfxInfo::GetAdapterSubsysID(nsAString&) { return NS_ERROR_FAILURE; }
NS_IMETHODIMP GfxInfo::GetAdapterSubsysID2(nsAString&) { return NS_ERROR_FAILURE; }
NS_IMETHODIMP GfxInfo::GetAdapterRAM(nsAString&) { return NS_ERROR_FAILURE; }
NS_IMETHODIMP GfxInfo::GetAdapterRAM2(nsAString&) { return NS_ERROR_FAILURE; }
NS_IMETHODIMP GfxInfo::GetAdapterDriverVersion(nsAString&) { return NS_ERROR_FAILURE; }
NS_IMETHODIMP GfxInfo::GetAdapterDriverVersion2(nsAString&) { return NS_ERROR_FAILURE; }
NS_IMETHODIMP GfxInfo::GetAdapterDriverDate(nsAString&) { return NS_ERROR_FAILURE; }
NS_IMETHODIMP GfxInfo::GetAdapterDriverDate2(nsAString&) { return NS_ERROR_FAILURE; }
NS_IMETHODIMP GfxInfo::GetIsGPU2Active(bool*) { return NS_ERROR_FAILURE; }
const nsTArray<GfxDriverInfo>&
GfxInfo::GetGfxDriverInfo()
{
// Empty blocklist: nothing to block, we always run software.
if (!mDriverInfo) {
mDriverInfo = new nsTArray<GfxDriverInfo>();
}
return *mDriverInfo;
}
nsresult
GfxInfo::GetFeatureStatusImpl(int32_t aFeature,
int32_t *aStatus,
nsAString & aSuggestedDriverVersion,
const nsTArray<GfxDriverInfo>& aDriverInfo,
nsACString& aFailureId,
OperatingSystem* aOS /* = nullptr */)
{
NS_ENSURE_ARG_POINTER(aStatus);
*aStatus = nsIGfxInfo::FEATURE_STATUS_UNKNOWN;
aSuggestedDriverVersion.SetIsVoid(true);
if (aOS) {
*aOS = OperatingSystem::Linux;
}
// Not blocklisted, not accelerated: leave the decision to the caller's
// software fallback.
return NS_OK;
}
#ifdef DEBUG
NS_IMETHODIMP GfxInfo::SpoofVendorID(const nsAString&) { return NS_ERROR_FAILURE; }
NS_IMETHODIMP GfxInfo::SpoofDeviceID(const nsAString&) { return NS_ERROR_FAILURE; }
NS_IMETHODIMP GfxInfo::SpoofDriverVersion(const nsAString&) { return NS_ERROR_FAILURE; }
NS_IMETHODIMP GfxInfo::SpoofOSVersion(uint32_t) { return NS_ERROR_FAILURE; }
#endif
} // namespace widget
} // namespace mozilla
+73
View File
@@ -0,0 +1,73 @@
/* -*- Mode: C++; tab-width: 2; 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/. */
#ifndef __mozilla_widget_headless_GfxInfo_h__
#define __mozilla_widget_headless_GfxInfo_h__
#include "GfxInfoBase.h"
#include "GfxDriverInfo.h"
#include "nsString.h"
namespace mozilla {
namespace widget {
// Headless GfxInfo: there is no GPU/driver to introspect. Every adapter query
// reports failure and every feature reports UNKNOWN (not blocklisted), so the
// gfx stack falls back to software rendering (BasicLayerManager). Init()/GetData()
// come from GfxInfoBase.
class GfxInfo final : public GfxInfoBase
{
public:
GfxInfo() {}
// We only declare the subset of nsIGfxInfo that GfxInfoBase leaves for the
// platform backend; the rest is inherited.
NS_IMETHOD GetD2DEnabled(bool *aD2DEnabled) override;
NS_IMETHOD GetDWriteEnabled(bool *aDWriteEnabled) override;
NS_IMETHOD GetDWriteVersion(nsAString & aDwriteVersion) override;
NS_IMETHOD GetCleartypeParameters(nsAString & aCleartypeParams) override;
NS_IMETHOD GetAdapterDescription(nsAString & aAdapterDescription) override;
NS_IMETHOD GetAdapterDriver(nsAString & aAdapterDriver) override;
NS_IMETHOD GetAdapterVendorID(nsAString & aAdapterVendorID) override;
NS_IMETHOD GetAdapterDeviceID(nsAString & aAdapterDeviceID) override;
NS_IMETHOD GetAdapterSubsysID(nsAString & aAdapterSubsysID) override;
NS_IMETHOD GetAdapterRAM(nsAString & aAdapterRAM) override;
NS_IMETHOD GetAdapterDriverVersion(nsAString & aAdapterDriverVersion) override;
NS_IMETHOD GetAdapterDriverDate(nsAString & aAdapterDriverDate) override;
NS_IMETHOD GetAdapterDescription2(nsAString & aAdapterDescription) override;
NS_IMETHOD GetAdapterDriver2(nsAString & aAdapterDriver) override;
NS_IMETHOD GetAdapterVendorID2(nsAString & aAdapterVendorID) override;
NS_IMETHOD GetAdapterDeviceID2(nsAString & aAdapterDeviceID) override;
NS_IMETHOD GetAdapterSubsysID2(nsAString & aAdapterSubsysID) override;
NS_IMETHOD GetAdapterRAM2(nsAString & aAdapterRAM) override;
NS_IMETHOD GetAdapterDriverVersion2(nsAString & aAdapterDriverVersion) override;
NS_IMETHOD GetAdapterDriverDate2(nsAString & aAdapterDriverDate) override;
NS_IMETHOD GetIsGPU2Active(bool *aIsGPU2Active) override;
using GfxInfoBase::GetFeatureStatus;
using GfxInfoBase::GetFeatureSuggestedDriverVersion;
using GfxInfoBase::GetWebGLParameter;
#ifdef DEBUG
NS_DECL_ISUPPORTS_INHERITED
NS_DECL_NSIGFXINFODEBUG
#endif
protected:
~GfxInfo() {}
virtual nsresult GetFeatureStatusImpl(int32_t aFeature,
int32_t *aStatus,
nsAString & aSuggestedDriverVersion,
const nsTArray<GfxDriverInfo>& aDriverInfo,
nsACString& aFailureId,
OperatingSystem* aOS = nullptr) override;
virtual const nsTArray<GfxDriverInfo>& GetGfxDriverInfo() override;
};
} // namespace widget
} // namespace mozilla
#endif /* __mozilla_widget_headless_GfxInfo_h__ */
+22
View File
@@ -0,0 +1,22 @@
# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*-
# 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/.
# Jihad: headless (no X/GTK) widget backend. The widget itself is the shared
# PuppetWidget (widget/PuppetWidget.cpp); this dir supplies the app shell and
# the component factory.
UNIFIED_SOURCES += [
'GfxInfo.cpp',
'nsAppShell.cpp',
'nsLookAndFeel.cpp',
'nsWidgetFactory.cpp',
]
include('/ipc/chromium/chromium-config.mozbuild')
FINAL_LIBRARY = 'xul'
LOCAL_INCLUDES += [
'/widget',
]
+27
View File
@@ -0,0 +1,27 @@
/* -*- Mode: C++; tab-width: 2; 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 "nsAppShell.h"
#include "nsThreadUtils.h"
using mozilla::NewRunnableMethod;
// There is no native event source in the headless backend. XPCOM events are
// processed via the XPCOM thread event queue (NS_ProcessNextEvent), so we wake
// the base app shell by posting a runnable that fires its NativeEventCallback.
void
nsAppShell::ScheduleNativeEventCallback()
{
nsCOMPtr<nsIRunnable> event =
NewRunnableMethod(this, &nsAppShell::NativeEventCallback);
NS_DispatchToCurrentThread(event.forget());
}
bool
nsAppShell::ProcessNextNativeEvent(bool aMayWait)
{
// No native events in a headless process.
return false;
}
+27
View File
@@ -0,0 +1,27 @@
/* -*- Mode: C++; tab-width: 2; 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/. */
// Jihad: headless app shell — no native (GTK/X) event loop. XPCOM events are
// driven by the embedder pumping NS_ProcessNextEvent; there are no native
// events to process.
#ifndef nsAppShell_h__
#define nsAppShell_h__
#include "nsBaseAppShell.h"
class nsAppShell : public nsBaseAppShell
{
public:
nsAppShell() {}
nsresult Init() { return nsBaseAppShell::Init(); }
virtual void ScheduleNativeEventCallback() override;
virtual bool ProcessNextNativeEvent(bool aMayWait) override;
protected:
virtual ~nsAppShell() {}
};
#endif // nsAppShell_h__
+33
View File
@@ -0,0 +1,33 @@
/* -*- Mode: C++; tab-width: 2; 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 "nsLookAndFeel.h"
nsLookAndFeel::nsLookAndFeel() : nsXPLookAndFeel()
{
}
nsLookAndFeel::~nsLookAndFeel()
{
}
nsresult
nsLookAndFeel::NativeGetColor(ColorID aID, nscolor& aResult)
{
// No native widget theme in headless mode. Use the built-in standin
// palette that nsXPLookAndFeel maintains for exactly this purpose.
aResult = GetStandinForNativeColor(aID);
return NS_OK;
}
bool
nsLookAndFeel::GetFontImpl(FontID aID, nsString& aFontName,
gfxFontStyle& aFontStyle,
float aDevPixPerCSSPixel)
{
// No system font database in headless mode. Report "no native font" so
// callers fall back to their default font selection.
return false;
}
+26
View File
@@ -0,0 +1,26 @@
/* -*- Mode: C++; tab-width: 2; 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/. */
#ifndef __nsLookAndFeel
#define __nsLookAndFeel
#include "nsXPLookAndFeel.h"
// Headless LookAndFeel: no native theme. Colors come from the cross-platform
// standin palette; the int/float/font queries fall through to the base
// nsXPLookAndFeel defaults.
class nsLookAndFeel final : public nsXPLookAndFeel
{
public:
nsLookAndFeel();
virtual ~nsLookAndFeel();
virtual nsresult NativeGetColor(ColorID aID, nscolor& aResult) override;
virtual bool GetFontImpl(FontID aID, nsString& aFontName,
gfxFontStyle& aFontStyle,
float aDevPixPerCSSPixel) override;
};
#endif
+65
View File
@@ -0,0 +1,65 @@
/* -*- Mode: C++; tab-width: 2; 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/. */
// Jihad: minimal headless widget factory. Registers the app shell, a fake
// screen manager (PuppetScreenManager), the shared transferable/format
// converter, and a stub GfxInfo (required: gfxPlatform::InitAcceleration()
// dereferences services::GetGfxInfo() at engine startup). Other widget
// components (theme, clipboard, idle, …) are still TODO.
#include "mozilla/ModuleUtils.h"
#include "nsWidgetsCID.h"
#include "nsAppShell.h"
#include "PuppetWidget.h" // PuppetScreenManager
#include "nsTransferable.h"
#include "nsHTMLFormatConverter.h"
#include "GfxInfo.h"
using namespace mozilla;
using namespace mozilla::widget;
NS_GENERIC_FACTORY_CONSTRUCTOR(nsAppShell)
NS_GENERIC_FACTORY_CONSTRUCTOR(PuppetScreenManager)
NS_GENERIC_FACTORY_CONSTRUCTOR(nsTransferable)
NS_GENERIC_FACTORY_CONSTRUCTOR(nsHTMLFormatConverter)
namespace mozilla {
namespace widget {
NS_GENERIC_FACTORY_CONSTRUCTOR_INIT(GfxInfo, Init)
}
}
NS_DEFINE_NAMED_CID(NS_APPSHELL_CID);
NS_DEFINE_NAMED_CID(NS_SCREENMANAGER_CID);
NS_DEFINE_NAMED_CID(NS_TRANSFERABLE_CID);
NS_DEFINE_NAMED_CID(NS_HTMLFORMATCONVERTER_CID);
NS_DEFINE_NAMED_CID(NS_GFXINFO_CID);
static const mozilla::Module::CIDEntry kWidgetCIDs[] = {
{ &kNS_APPSHELL_CID, false, nullptr, nsAppShellConstructor, Module::ALLOW_IN_GPU_PROCESS },
{ &kNS_SCREENMANAGER_CID, false, nullptr, PuppetScreenManagerConstructor, Module::MAIN_PROCESS_ONLY },
{ &kNS_TRANSFERABLE_CID, false, nullptr, nsTransferableConstructor },
{ &kNS_HTMLFORMATCONVERTER_CID, false, nullptr, nsHTMLFormatConverterConstructor },
{ &kNS_GFXINFO_CID, false, nullptr, mozilla::widget::GfxInfoConstructor },
{ nullptr }
};
static const mozilla::Module::ContractIDEntry kWidgetContracts[] = {
{ "@mozilla.org/widget/appshell/headless;1", &kNS_APPSHELL_CID },
{ "@mozilla.org/gfx/screenmanager;1", &kNS_SCREENMANAGER_CID },
{ "@mozilla.org/widget/transferable;1", &kNS_TRANSFERABLE_CID },
{ "@mozilla.org/widget/htmlformatconverter;1", &kNS_HTMLFORMATCONVERTER_CID },
{ "@mozilla.org/gfx/info;1", &kNS_GFXINFO_CID },
{ nullptr }
};
static const mozilla::Module kWidgetModule = {
mozilla::Module::kVersion,
kWidgetCIDs,
kWidgetContracts
};
NSMODULE_DEFN(nsWidgetHeadlessModule) = &kWidgetModule;
+7 -2
View File
@@ -52,9 +52,14 @@ if toolkit in ('cocoa', 'gtk2', 'gtk3'):
if 'gtk' in CONFIG['MOZ_WIDGET_TOOLKIT']:
DIRS += ['gtk']
if CONFIG['MOZ_X11'] and CONFIG['MOZ_ENABLE_NPAPI']:
if (CONFIG['MOZ_X11'] and CONFIG['MOZ_ENABLE_NPAPI'] and
CONFIG['MOZ_ENABLE_NPAPI_GTK2']):
DIRS += ['gtkxtbin']
# Jihad: X/GTK-free headless widget backend (PuppetWidget-based).
if toolkit == 'headless':
DIRS += ['headless']
XPIDL_SOURCES += [
'nsIAppShell.idl',
'nsIBaseWindow.idl',
@@ -228,7 +233,7 @@ if toolkit in ('cocoa', 'windows'):
'nsBaseClipboard.cpp',
]
if toolkit in {'gtk2', 'gtk3', 'cocoa', 'windows'}:
if toolkit in {'gtk2', 'gtk3', 'cocoa', 'windows', 'headless'}:
UNIFIED_SOURCES += [
'nsBaseFilePicker.cpp',
'nsNativeTheme.cpp',
+14 -1
View File
@@ -531,7 +531,20 @@ BackgroundHangMonitor::BackgroundHangMonitor(const char* aName,
: mThread(aThreadType == THREAD_SHARED ? BackgroundHangThread::FindThread() : nullptr)
{
#ifdef MOZ_ENABLE_BACKGROUND_HANG_MONITOR
if (!BackgroundHangManager::sDisabled && !mThread) {
// sInstance is null in a process that never initialized XPCOM — Startup() is called from
// NS_InitXPCOM2/NS_InitMinimalXPCOM and from nowhere else, and XRE_InitChildProcess calls
// neither. sDisabled does NOT cover that case: it is only set by Shutdown(), so in a
// never-started process it is false while sInstance is null, and BackgroundHangThread's
// constructor dereferences the manager immediately (mInterval(mManager->mIntervalNow)) for
// an instant null crash. FindThread() already makes exactly this check; the named
// constructor is missing it.
//
// Reachable here because MessagePumpDefault::Run constructs one of these unconditionally,
// and the plugin child uses a TYPE_UI loop, which on a non-GTK build is a MessagePumpDefault
// rather than the glib pump upstream would get. Result: the NPAPI child died with SIGSEGV
// the instant it entered its message loop, before answering NP_Initialize.
if (!BackgroundHangManager::sDisabled && !mThread &&
BackgroundHangManager::sInstance) {
mThread = new BackgroundHangThread(aName, aTimeoutMs, aMaxTimeoutMs,
aThreadType);
}
+14
View File
@@ -4,6 +4,20 @@ dnl This Source Code Form is subject to the terms of the Mozilla Public
dnl License, v. 2.0. If a copy of the MPL was not distributed with this
dnl file, You can obtain one at http://mozilla.org/MPL/2.0/.
dnl ========================================================
dnl = Enable AppCompat GUID Support (mirrors palemoon/configure.in:19-28)
dnl ========================================================
MOZ_ARG_ENABLE_BOOL(appcompat-guid,
[ --enable-appcompat-guid Enable AppCompat GUID Support],
UXP_APPCOMPAT_GUID=1,
UXP_APPCOMPAT_GUID=)
if test -n "$UXP_APPCOMPAT_GUID"; then
AC_DEFINE(UXP_APPCOMPAT_GUID)
fi
AC_SUBST(UXP_APPCOMPAT_GUID)
dnl Things we need to carry from confvars.sh
AC_SUBST(MOZ_XULRUNNER)
AC_DEFINE(MOZ_XULRUNNER)
+11 -1
View File
@@ -6,4 +6,14 @@
MOZ_APP_NAME=xulrunner
MOZ_APP_DISPLAYNAME=XULRunner
MOZ_APP_VERSION=$MOZILLA_VERSION
MOZ_XULRUNNER=1
MOZ_XULRUNNER=1
# Jihad: AppCompat GUID (Pale Moon's UXP_APPCOMPAT_GUID feature, palemoon/confvars.sh:71).
# Our app GUID is frozen and named by ZERO existing extensions, so without this every real XPI
# arrives appDisabled and the web-install path treats that as a failed install
# (amWebInstallListener.js:112-115) -- i.e. XPI support is structurally unreachable. With it,
# XPIProvider.jsm accepts a targetApplication naming extensions.guid.appCompatId and
# substitutes extensions.guid.appCompatVersion (XPIProvider.jsm:6357-6377, extensions only --
# themes are deliberately excluded upstream). Basilisk solves the same problem by shipping
# Firefox's GUID outright; that is not open to us, the app ID is frozen by user decision.
UXP_APPCOMPAT_GUID=1